diff --git a/database/factories/ComponentGroupFactory.php b/database/factories/ComponentGroupFactory.php
index c5266093..ef1a103d 100644
--- a/database/factories/ComponentGroupFactory.php
+++ b/database/factories/ComponentGroupFactory.php
@@ -3,6 +3,8 @@
namespace Cachet\Database\Factories;
use Cachet\Enums\ComponentGroupVisibilityEnum;
+use Cachet\Enums\ResourceOrderColumnEnum;
+use Cachet\Enums\ResourceOrderDirectionEnum;
use Cachet\Models\ComponentGroup;
use Illuminate\Database\Eloquent\Factories\Factory;
@@ -23,7 +25,20 @@ public function definition(): array
return [
'name' => fake()->word,
'order' => 0,
+ 'order_column' => ResourceOrderColumnEnum::Manual,
+ 'order_direction' => null,
'visible' => ComponentGroupVisibilityEnum::expanded->value,
];
}
+
+ /**
+ * Order the group's components by the given column and direction.
+ */
+ public function orderedBy(ResourceOrderColumnEnum $column, ?ResourceOrderDirectionEnum $direction = ResourceOrderDirectionEnum::Asc): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'order_column' => $column,
+ 'order_direction' => $column === ResourceOrderColumnEnum::Manual ? null : $direction,
+ ]);
+ }
}
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..cacf4cc0
--- /dev/null
+++ b/database/migrations/2025_01_21_121458_add_ordering_cols_to_component_groups_table.php
@@ -0,0 +1,36 @@
+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/de/component_group.php b/resources/lang/de/component_group.php
index e9c11373..59538566 100644
--- a/resources/lang/de/component_group.php
+++ b/resources/lang/de/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Name',
'visible' => 'Sichtbar',
'collapsed' => 'Ausgeklappt',
+ 'order_column' => 'Sortierung der Komponentengruppe',
'created_at' => 'Erstellt am',
'updated_at' => 'Aktualisiert am',
],
@@ -25,5 +26,7 @@
'name_label' => 'Name',
'visible_label' => 'Sichtbar',
'collapsed_label' => 'Ausgeklappt',
+ 'order_column_label' => 'Sortierung der Komponentengruppe',
+ 'order_direction' => 'Sortierrichtung',
],
];
diff --git a/resources/lang/de/resource.php b/resources/lang/de/resource.php
index 41a7fef2..6354b59b 100644
--- a/resources/lang/de/resource.php
+++ b/resources/lang/de/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Zuletzt aktualisiert',
+ 'name' => 'Name',
+ 'manual' => 'Manuell',
+ 'status' => 'Status',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Aufsteigend',
+ 'desc' => 'Absteigend',
+ ],
'visibility' => [
'authenticated' => 'Benutzer',
'guest' => 'Gäste',
diff --git a/resources/lang/de_AT/component_group.php b/resources/lang/de_AT/component_group.php
index e9c11373..59538566 100644
--- a/resources/lang/de_AT/component_group.php
+++ b/resources/lang/de_AT/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Name',
'visible' => 'Sichtbar',
'collapsed' => 'Ausgeklappt',
+ 'order_column' => 'Sortierung der Komponentengruppe',
'created_at' => 'Erstellt am',
'updated_at' => 'Aktualisiert am',
],
@@ -25,5 +26,7 @@
'name_label' => 'Name',
'visible_label' => 'Sichtbar',
'collapsed_label' => 'Ausgeklappt',
+ 'order_column_label' => 'Sortierung der Komponentengruppe',
+ 'order_direction' => 'Sortierrichtung',
],
];
diff --git a/resources/lang/de_AT/resource.php b/resources/lang/de_AT/resource.php
index 41a7fef2..6354b59b 100644
--- a/resources/lang/de_AT/resource.php
+++ b/resources/lang/de_AT/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Zuletzt aktualisiert',
+ 'name' => 'Name',
+ 'manual' => 'Manuell',
+ 'status' => 'Status',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Aufsteigend',
+ 'desc' => 'Absteigend',
+ ],
'visibility' => [
'authenticated' => 'Benutzer',
'guest' => 'Gäste',
diff --git a/resources/lang/de_CH/component_group.php b/resources/lang/de_CH/component_group.php
index e9c11373..59538566 100644
--- a/resources/lang/de_CH/component_group.php
+++ b/resources/lang/de_CH/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Name',
'visible' => 'Sichtbar',
'collapsed' => 'Ausgeklappt',
+ 'order_column' => 'Sortierung der Komponentengruppe',
'created_at' => 'Erstellt am',
'updated_at' => 'Aktualisiert am',
],
@@ -25,5 +26,7 @@
'name_label' => 'Name',
'visible_label' => 'Sichtbar',
'collapsed_label' => 'Ausgeklappt',
+ 'order_column_label' => 'Sortierung der Komponentengruppe',
+ 'order_direction' => 'Sortierrichtung',
],
];
diff --git a/resources/lang/de_CH/resource.php b/resources/lang/de_CH/resource.php
index 41a7fef2..6354b59b 100644
--- a/resources/lang/de_CH/resource.php
+++ b/resources/lang/de_CH/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Zuletzt aktualisiert',
+ 'name' => 'Name',
+ 'manual' => 'Manuell',
+ 'status' => 'Status',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Aufsteigend',
+ 'desc' => 'Absteigend',
+ ],
'visibility' => [
'authenticated' => 'Benutzer',
'guest' => 'Gäste',
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/lang/es_ES/component_group.php b/resources/lang/es_ES/component_group.php
index dff85814..0afd845a 100644
--- a/resources/lang/es_ES/component_group.php
+++ b/resources/lang/es_ES/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Nombre',
'visible' => 'Visible',
'collapsed' => 'Colapsado',
+ 'order_column' => 'Orden del Grupo de Componentes',
'created_at' => 'Creado el',
'updated_at' => 'Actualizado el',
],
@@ -25,5 +26,7 @@
'name_label' => 'Nombre',
'visible_label' => 'Visible',
'collapsed_label' => 'Colapsado',
+ 'order_column_label' => 'Orden del Grupo de Componentes',
+ 'order_direction' => 'Dirección del Orden',
],
];
diff --git a/resources/lang/es_ES/resource.php b/resources/lang/es_ES/resource.php
index eda34d2c..0ddaa37d 100644
--- a/resources/lang/es_ES/resource.php
+++ b/resources/lang/es_ES/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Última Actualización',
+ 'name' => 'Nombre',
+ 'manual' => 'Manual',
+ 'status' => 'Estado',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Ascendente',
+ 'desc' => 'Descendente',
+ ],
'visibility' => [
'authenticated' => 'Usuarios',
'guest' => 'Invitados',
diff --git a/resources/lang/fr/component_group.php b/resources/lang/fr/component_group.php
index 3d5bb245..79b0a9a0 100644
--- a/resources/lang/fr/component_group.php
+++ b/resources/lang/fr/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Nom',
'visible' => 'Visible',
'collapsed' => 'Réduit',
+ 'order_column' => 'Ordre du groupe de composants',
'created_at' => 'Créé le',
'updated_at' => 'Mis à jour le',
],
@@ -25,5 +26,7 @@
'name_label' => 'Nom',
'visible_label' => 'Visible',
'collapsed_label' => 'Réduit',
+ 'order_column_label' => 'Ordre du groupe de composants',
+ 'order_direction' => 'Sens du tri',
],
];
diff --git a/resources/lang/fr/resource.php b/resources/lang/fr/resource.php
index 3bbac064..dc4444fd 100644
--- a/resources/lang/fr/resource.php
+++ b/resources/lang/fr/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Dernière mise à jour',
+ 'name' => 'Nom',
+ 'manual' => 'Manuel',
+ 'status' => 'Statut',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Croissant',
+ 'desc' => 'Décroissant',
+ ],
'visibility' => [
'authenticated' => 'Utilisateurs',
'guest' => 'Invités',
diff --git a/resources/lang/ko/component_group.php b/resources/lang/ko/component_group.php
index ff02443a..9736a733 100644
--- a/resources/lang/ko/component_group.php
+++ b/resources/lang/ko/component_group.php
@@ -13,6 +13,7 @@
'name' => '이름',
'visible' => '표시 여부',
'collapsed' => '축소 여부',
+ 'order_column' => '구성 요소 그룹 정렬',
'created_at' => '생성 시간',
'updated_at' => '업데이트 시간',
],
@@ -25,5 +26,7 @@
'name_label' => '이름',
'visible_label' => '표시 여부',
'collapsed_label' => '축소 여부',
+ 'order_column_label' => '구성 요소 그룹 정렬',
+ 'order_direction' => '정렬 방향',
],
];
diff --git a/resources/lang/ko/resource.php b/resources/lang/ko/resource.php
index caa2f6ee..f473e877 100644
--- a/resources/lang/ko/resource.php
+++ b/resources/lang/ko/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => '마지막 업데이트',
+ 'name' => '이름',
+ 'manual' => '수동',
+ 'status' => '상태',
+ ],
+ 'order_direction' => [
+ 'asc' => '오름차순',
+ 'desc' => '내림차순',
+ ],
'visibility' => [
'authenticated' => '사용자',
'guest' => '게스트',
diff --git a/resources/lang/nl/component_group.php b/resources/lang/nl/component_group.php
index 836d3dcb..9de2a9e3 100644
--- a/resources/lang/nl/component_group.php
+++ b/resources/lang/nl/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Naam',
'visible' => 'Zichtbaar',
'collapsed' => 'Uitgevouwen',
+ 'order_column' => 'Volgorde componentgroep',
'created_at' => 'Gemaakt op',
'updated_at' => 'Bijgewerkt op',
],
@@ -25,5 +26,7 @@
'name_label' => 'Naam',
'visible_label' => 'Zichtbaar',
'collapsed_label' => 'Uitgevouwen',
+ 'order_column_label' => 'Volgorde componentgroep',
+ 'order_direction' => 'Sorteerrichting',
],
];
diff --git a/resources/lang/nl/resource.php b/resources/lang/nl/resource.php
index 747a4d9c..95a0655e 100644
--- a/resources/lang/nl/resource.php
+++ b/resources/lang/nl/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Laatst bijgewerkt',
+ 'name' => 'Naam',
+ 'manual' => 'Handmatig',
+ 'status' => 'Status',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Oplopend',
+ 'desc' => 'Aflopend',
+ ],
'visibility' => [
'authenticated' => 'Gebruiker',
'guest' => 'Gasten',
diff --git a/resources/lang/ph/component_group.php b/resources/lang/ph/component_group.php
index 63a8b058..8d169258 100644
--- a/resources/lang/ph/component_group.php
+++ b/resources/lang/ph/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Pangalan',
'visible' => 'Nakikita',
'collapsed' => 'Nakasara',
+ 'order_column' => 'Pagkakasunod-sunod ng Grupo ng Komponent',
'created_at' => 'Ginawa Noong',
'updated_at' => 'Na-update Noong',
],
@@ -25,5 +26,7 @@
'name_label' => 'Pangalan',
'visible_label' => 'Nakikita',
'collapsed_label' => 'Nakasara',
+ 'order_column_label' => 'Pagkakasunod-sunod ng Grupo ng Komponent',
+ 'order_direction' => 'Direksyon ng Pagkakasunod-sunod',
],
];
diff --git a/resources/lang/ph/resource.php b/resources/lang/ph/resource.php
index c3f5c556..cc3a4d43 100644
--- a/resources/lang/ph/resource.php
+++ b/resources/lang/ph/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Huling Na-update',
+ 'name' => 'Pangalan',
+ 'manual' => 'Manu-mano',
+ 'status' => 'Katayuan',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Pataas',
+ 'desc' => 'Pababa',
+ ],
'visibility' => [
'authenticated' => 'Mga Gumagamit',
'guest' => 'Mga Bisita',
diff --git a/resources/lang/pt_BR/component_group.php b/resources/lang/pt_BR/component_group.php
index c96bf377..7a81e198 100644
--- a/resources/lang/pt_BR/component_group.php
+++ b/resources/lang/pt_BR/component_group.php
@@ -13,6 +13,7 @@
'name' => 'Nome',
'visible' => 'Visível',
'collapsed' => 'Recolhido',
+ 'order_column' => 'Ordem do Grupo de Componentes',
'created_at' => 'Criado em',
'updated_at' => 'Atualizado em',
],
@@ -25,5 +26,7 @@
'name_label' => 'Nome',
'visible_label' => 'Visível',
'collapsed_label' => 'Recolhido',
+ 'order_column_label' => 'Ordem do Grupo de Componentes',
+ 'order_direction' => 'Direção da Ordenação',
],
];
diff --git a/resources/lang/pt_BR/resource.php b/resources/lang/pt_BR/resource.php
index 1e679b42..6706c6d3 100644
--- a/resources/lang/pt_BR/resource.php
+++ b/resources/lang/pt_BR/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => 'Última Atualização',
+ 'name' => 'Nome',
+ 'manual' => 'Manual',
+ 'status' => 'Status',
+ ],
+ 'order_direction' => [
+ 'asc' => 'Crescente',
+ 'desc' => 'Decrescente',
+ ],
'visibility' => [
'authenticated' => 'Usuários',
'guest' => 'Visitantes',
diff --git a/resources/lang/zh_CN/component_group.php b/resources/lang/zh_CN/component_group.php
index 043403b0..461e576e 100644
--- a/resources/lang/zh_CN/component_group.php
+++ b/resources/lang/zh_CN/component_group.php
@@ -13,6 +13,7 @@
'name' => '名称',
'visible' => '可见',
'collapsed' => '折叠',
+ 'order_column' => '组件组排序',
'created_at' => '创建时间',
'updated_at' => '更新时间',
],
@@ -25,5 +26,7 @@
'name_label' => '名称',
'visible_label' => '可见',
'collapsed_label' => '折叠',
+ 'order_column_label' => '组件组排序',
+ 'order_direction' => '排序方向',
],
];
diff --git a/resources/lang/zh_CN/resource.php b/resources/lang/zh_CN/resource.php
index adf83e87..17706339 100644
--- a/resources/lang/zh_CN/resource.php
+++ b/resources/lang/zh_CN/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => '最后更新',
+ 'name' => '名称',
+ 'manual' => '手动',
+ 'status' => '状态',
+ ],
+ 'order_direction' => [
+ 'asc' => '升序',
+ 'desc' => '降序',
+ ],
'visibility' => [
'authenticated' => '用户',
'guest' => '游客',
diff --git a/resources/lang/zh_TW/component_group.php b/resources/lang/zh_TW/component_group.php
index 652789d6..8f528d5f 100644
--- a/resources/lang/zh_TW/component_group.php
+++ b/resources/lang/zh_TW/component_group.php
@@ -13,6 +13,7 @@
'name' => '名稱',
'visible' => '可見',
'collapsed' => '摺疊',
+ 'order_column' => '組件組排序',
'created_at' => '創建時間',
'updated_at' => '更新時間',
],
@@ -25,5 +26,7 @@
'name_label' => '名稱',
'visible_label' => '可見',
'collapsed_label' => '摺疊',
+ 'order_column_label' => '組件組排序',
+ 'order_direction' => '排序方向',
],
];
diff --git a/resources/lang/zh_TW/resource.php b/resources/lang/zh_TW/resource.php
index 4c333877..2955dc51 100644
--- a/resources/lang/zh_TW/resource.php
+++ b/resources/lang/zh_TW/resource.php
@@ -1,6 +1,17 @@
[
+ 'id' => 'ID',
+ 'last_updated' => '最後更新',
+ 'name' => '名稱',
+ 'manual' => '手動',
+ 'status' => '狀態',
+ ],
+ 'order_direction' => [
+ 'asc' => '升序',
+ 'desc' => '降序',
+ ],
'visibility' => [
'authenticated' => '用戶',
'guest' => '遊客',
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 b859887f..5a75a23f 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 ($display_graphs)
@endif
diff --git a/src/Data/Requests/ComponentGroup/CreateComponentGroupRequestData.php b/src/Data/Requests/ComponentGroup/CreateComponentGroupRequestData.php
index 476e5997..ef371de3 100644
--- a/src/Data/Requests/ComponentGroup/CreateComponentGroupRequestData.php
+++ b/src/Data/Requests/ComponentGroup/CreateComponentGroupRequestData.php
@@ -4,6 +4,8 @@
use Cachet\Data\BaseData;
use Cachet\Enums\ComponentGroupVisibilityEnum;
+use Cachet\Enums\ResourceOrderColumnEnum;
+use Cachet\Enums\ResourceOrderDirectionEnum;
use Cachet\Enums\ResourceVisibilityEnum;
use Illuminate\Validation\Rule;
use Spatie\LaravelData\Support\Validation\ValidationContext;
@@ -15,6 +17,8 @@ public function __construct(
public readonly ?int $order = null,
public readonly ?ResourceVisibilityEnum $visible = null,
public readonly ?ComponentGroupVisibilityEnum $collapsed = null,
+ public readonly ?ResourceOrderColumnEnum $orderColumn = null,
+ public readonly ?ResourceOrderDirectionEnum $orderDirection = null,
public readonly ?array $components = null,
) {}
@@ -25,6 +29,16 @@ public static function rules(ValidationContext $context): array
'order' => ['int', 'min:0'],
'visible' => ['bool'],
'collapsed' => [Rule::enum(ComponentGroupVisibilityEnum::class)],
+ 'order_column' => [Rule::enum(ResourceOrderColumnEnum::class)],
+ 'order_direction' => [
+ 'nullable',
+ Rule::requiredIf(function () use ($context) {
+ $column = $context->payload['order_column'] ?? null;
+
+ return filled($column) && $column !== ResourceOrderColumnEnum::Manual->value;
+ }),
+ Rule::enum(ResourceOrderDirectionEnum::class),
+ ],
'components' => ['array'],
'components.*' => ['int', 'min:0', Rule::exists('components', 'id')],
];
diff --git a/src/Data/Requests/ComponentGroup/UpdateComponentGroupRequestData.php b/src/Data/Requests/ComponentGroup/UpdateComponentGroupRequestData.php
index b9177e5b..9a113c01 100644
--- a/src/Data/Requests/ComponentGroup/UpdateComponentGroupRequestData.php
+++ b/src/Data/Requests/ComponentGroup/UpdateComponentGroupRequestData.php
@@ -4,6 +4,8 @@
use Cachet\Data\BaseData;
use Cachet\Enums\ComponentGroupVisibilityEnum;
+use Cachet\Enums\ResourceOrderColumnEnum;
+use Cachet\Enums\ResourceOrderDirectionEnum;
use Illuminate\Validation\Rule;
use Spatie\LaravelData\Support\Validation\ValidationContext;
@@ -14,6 +16,8 @@ public function __construct(
public readonly ?int $order = null,
public readonly ?bool $visible = null,
public readonly ?ComponentGroupVisibilityEnum $collapsed = null,
+ public readonly ?ResourceOrderColumnEnum $orderColumn = null,
+ public readonly ?ResourceOrderDirectionEnum $orderDirection = null,
public readonly ?array $components = null,
) {}
@@ -24,6 +28,16 @@ public static function rules(ValidationContext $context): array
'order' => ['int', 'min:0'],
'visible' => ['bool'],
'collapsed' => [Rule::enum(ComponentGroupVisibilityEnum::class)],
+ 'order_column' => [Rule::enum(ResourceOrderColumnEnum::class)],
+ 'order_direction' => [
+ 'nullable',
+ Rule::requiredIf(function () use ($context) {
+ $column = $context->payload['order_column'] ?? null;
+
+ return filled($column) && $column !== ResourceOrderColumnEnum::Manual->value;
+ }),
+ Rule::enum(ResourceOrderDirectionEnum::class),
+ ],
'components' => ['array'],
'components.*' => ['int', 'min:0', Rule::exists('components', 'id')],
];
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/ComponentGroups/ComponentGroupResource.php b/src/Filament/Resources/ComponentGroups/ComponentGroupResource.php
index 214b1359..a7593701 100644
--- a/src/Filament/Resources/ComponentGroups/ComponentGroupResource.php
+++ b/src/Filament/Resources/ComponentGroups/ComponentGroupResource.php
@@ -3,6 +3,8 @@
namespace Cachet\Filament\Resources\ComponentGroups;
use Cachet\Enums\ComponentGroupVisibilityEnum;
+use Cachet\Enums\ResourceOrderColumnEnum;
+use Cachet\Enums\ResourceOrderDirectionEnum;
use Cachet\Enums\ResourceVisibilityEnum;
use Cachet\Filament\Resources\ComponentGroups\Pages\CreateComponentGroup;
use Cachet\Filament\Resources\ComponentGroups\Pages\EditComponentGroup;
@@ -12,10 +14,12 @@
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
+use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
+use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -49,9 +53,22 @@ public static function form(Schema $schema): Schema
->required()
->inline()
->options(ComponentGroupVisibilityEnum::class)
- ->default(ComponentGroupVisibilityEnum::expanded)
+ ->default(ComponentGroupVisibilityEnum::expanded->value)
->columnSpanFull(),
]),
+ Section::make()->schema([
+ Select::make('order_column')
+ ->label(__('cachet::component_group.form.order_column_label'))
+ ->options(ResourceOrderColumnEnum::class)
+ ->default(ResourceOrderColumnEnum::Manual->value)
+ ->required()
+ ->live(),
+ Select::make('order_direction')
+ ->label(__('cachet::component_group.form.order_direction'))
+ ->options(ResourceOrderDirectionEnum::class)
+ ->required(fn (Get $get) => $get('order_column') !== ResourceOrderColumnEnum::Manual->value)
+ ->visible(fn (Get $get) => $get('order_column') !== ResourceOrderColumnEnum::Manual->value),
+ ]),
]);
}
@@ -70,6 +87,15 @@ public static function table(Table $table): Table
TextColumn::make('collapsed')
->label(__('cachet::component_group.list.headers.collapsed'))
->sortable(),
+ 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(),
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 c9b34438..3d9ee57b 100644
--- a/src/Http/Controllers/StatusPage/StatusPageController.php
+++ b/src/Http/Controllers/StatusPage/StatusPageController.php
@@ -2,12 +2,9 @@
namespace Cachet\Http\Controllers\StatusPage;
-use Cachet\Models\Component;
-use Cachet\Models\ComponentGroup;
use Cachet\Models\Incident;
use Cachet\Models\Schedule;
use Cachet\Settings\AppSettings;
-use Illuminate\Database\Eloquent\Builder;
use Illuminate\View\View;
class StatusPageController
@@ -26,19 +23,6 @@ public function __construct(protected AppSettings $appSettings)
public function index(): View
{
return view('cachet::status-page.index', [
- 'componentGroups' => ComponentGroup::query()
- ->with(['components' => fn ($query) => $query->enabled()->orderBy('order')->withCount(['incidents' => fn ($q) => $q->unresolved()])])
- ->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' => fn ($query) => $query->unresolved()])
- ->get(),
-
'schedules' => Schedule::query()->with(['updates', 'components'])->incomplete()->orderBy('scheduled_at')->get(),
'display_graphs' => $this->appSettings->display_graphs,
diff --git a/src/Http/Resources/ComponentGroup.php b/src/Http/Resources/ComponentGroup.php
index 5d24cf3a..ceab07a9 100644
--- a/src/Http/Resources/ComponentGroup.php
+++ b/src/Http/Resources/ComponentGroup.php
@@ -14,6 +14,8 @@ public function toAttributes(Request $request): array
'id' => $this->id,
'name' => $this->name,
'order' => $this->order,
+ 'order_column' => $this->order_column,
+ 'order_direction' => $this->order_direction,
'collapsed' => $this->collapsed,
'visible' => $this->visible,
'created' => [
diff --git a/src/Models/Component.php b/src/Models/Component.php
index b8a46ddc..250433ea 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;
@@ -151,6 +152,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 92ef0a06..bb755c20 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;
@@ -35,6 +37,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 +47,8 @@ class ComponentGroup extends Model
protected $fillable = [
'name',
'order',
+ 'order_column',
+ 'order_direction',
'collapsed',
'visible',
];
@@ -61,7 +67,7 @@ protected static function booted(): void
*/
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 a02da2f9..b28fc9e3 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..1e113449
--- /dev/null
+++ b/src/View/Components/ComponentGroups.php
@@ -0,0 +1,48 @@
+ $this->componentGroups(),
+ 'ungroupedComponents' => Component::query()
+ ->enabled()
+ ->whereNull('component_group_id')
+ ->orderBy('order')
+ ->withCount(['incidents' => fn ($query) => $query->unresolved()])
+ ->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' => fn ($query) => $query->unresolved()])])
+ ->visible(auth()->check())
+ ->orderBy('order')
+ ->when(auth()->check(), fn (Builder $query) => $query->users(), fn ($query) => $query->guests())
+ ->get()
+ ->map(function (ComponentGroup $group) {
+ $group->setRelation('components', $group->components->sortBy(
+ fn (Component $component) => $component->orderableBy($group),
+ descending: $group->order_direction?->descending() ?? false,
+ )->values());
+
+ return $group;
+ });
+ }
+}
diff --git a/tests/Feature/Api/ComponentGroupTest.php b/tests/Feature/Api/ComponentGroupTest.php
index 4d910919..28cbe643 100644
--- a/tests/Feature/Api/ComponentGroupTest.php
+++ b/tests/Feature/Api/ComponentGroupTest.php
@@ -1,6 +1,8 @@
null,
]);
});
+
+it('exposes the order column and direction in the resource', function () {
+ $componentGroup = ComponentGroup::factory()
+ ->orderedBy(ResourceOrderColumnEnum::Name, ResourceOrderDirectionEnum::Desc)
+ ->create();
+
+ $response = getJson('/status/api/component-groups/'.$componentGroup->id);
+
+ $response->assertOk();
+ $response->assertJsonFragment([
+ 'order_column' => ResourceOrderColumnEnum::Name->value,
+ 'order_direction' => ResourceOrderDirectionEnum::Desc->value,
+ ]);
+});
+
+it('can create a component group with a manual order column', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $response = postJson('/status/api/component-groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Manual->value,
+ ]);
+
+ $response->assertCreated();
+ $this->assertDatabaseHas('component_groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Manual->value,
+ 'order_direction' => null,
+ ]);
+});
+
+it('can create a component group with an order column and direction', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $response = postJson('/status/api/component-groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Name->value,
+ 'order_direction' => ResourceOrderDirectionEnum::Asc->value,
+ ]);
+
+ $response->assertCreated();
+ $this->assertDatabaseHas('component_groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Name->value,
+ 'order_direction' => ResourceOrderDirectionEnum::Asc->value,
+ ]);
+});
+
+it('requires an order direction when the order column is not manual', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $response = postJson('/status/api/component-groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Status->value,
+ ]);
+
+ $response->assertUnprocessable();
+ $response->assertJsonValidationErrors('order_direction');
+});
+
+it('does not require an order direction when the order column is manual', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $response = postJson('/status/api/component-groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Manual->value,
+ ]);
+
+ $response->assertCreated();
+});
+
+it('cannot create a component group with an invalid order column', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $response = postJson('/status/api/component-groups', [
+ 'name' => 'New Group',
+ 'order_column' => 'not-a-column',
+ ]);
+
+ $response->assertUnprocessable();
+ $response->assertJsonValidationErrors('order_column');
+});
+
+it('cannot create a component group with an invalid order direction', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $response = postJson('/status/api/component-groups', [
+ 'name' => 'New Group',
+ 'order_column' => ResourceOrderColumnEnum::Name->value,
+ 'order_direction' => 'sideways',
+ ]);
+
+ $response->assertUnprocessable();
+ $response->assertJsonValidationErrors('order_direction');
+});
+
+it('can update a component group order column and direction', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $componentGroup = ComponentGroup::factory()->create([
+ 'order_column' => ResourceOrderColumnEnum::Manual,
+ ]);
+
+ $response = putJson('/status/api/component-groups/'.$componentGroup->id, [
+ 'order_column' => ResourceOrderColumnEnum::LastUpdated->value,
+ 'order_direction' => ResourceOrderDirectionEnum::Desc->value,
+ ]);
+
+ $response->assertOk();
+ $this->assertDatabaseHas('component_groups', [
+ 'id' => $componentGroup->id,
+ 'order_column' => ResourceOrderColumnEnum::LastUpdated->value,
+ 'order_direction' => ResourceOrderDirectionEnum::Desc->value,
+ ]);
+});
+
+it('can update an unrelated attribute without supplying an order direction', function () {
+ Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']);
+
+ $componentGroup = ComponentGroup::factory()->create([
+ 'order_column' => ResourceOrderColumnEnum::Manual,
+ ]);
+
+ $response = putJson('/status/api/component-groups/'.$componentGroup->id, [
+ 'name' => 'Renamed Group',
+ ]);
+
+ $response->assertOk();
+ $this->assertDatabaseHas('component_groups', [
+ 'id' => $componentGroup->id,
+ 'name' => 'Renamed Group',
+ ]);
+});
diff --git a/tests/Feature/StatusPage/ComponentGroupOrderingTest.php b/tests/Feature/StatusPage/ComponentGroupOrderingTest.php
new file mode 100644
index 00000000..e2cd2a8d
--- /dev/null
+++ b/tests/Feature/StatusPage/ComponentGroupOrderingTest.php
@@ -0,0 +1,62 @@
+orderedBy($column, $direction)
+ ->create(['visible' => ResourceVisibilityEnum::guest]);
+}
+
+it('orders a group\'s components by name ascending', function () {
+ $group = orderingGroup(ResourceOrderColumnEnum::Name, ResourceOrderDirectionEnum::Asc);
+
+ Component::factory()->for($group, 'group')->create(['name' => 'CharlieSvc']);
+ Component::factory()->for($group, 'group')->create(['name' => 'AlphaSvc']);
+ Component::factory()->for($group, 'group')->create(['name' => 'BravoSvc']);
+
+ $this->get(route('cachet.status-page'))
+ ->assertOk()
+ ->assertSeeInOrder(['AlphaSvc', 'BravoSvc', 'CharlieSvc']);
+});
+
+it('orders a group\'s components by name descending', function () {
+ $group = orderingGroup(ResourceOrderColumnEnum::Name, ResourceOrderDirectionEnum::Desc);
+
+ Component::factory()->for($group, 'group')->create(['name' => 'CharlieSvc']);
+ Component::factory()->for($group, 'group')->create(['name' => 'AlphaSvc']);
+ Component::factory()->for($group, 'group')->create(['name' => 'BravoSvc']);
+
+ $this->get(route('cachet.status-page'))
+ ->assertOk()
+ ->assertSeeInOrder(['CharlieSvc', 'BravoSvc', 'AlphaSvc']);
+});
+
+it('orders a group\'s components by status ascending', function () {
+ $group = orderingGroup(ResourceOrderColumnEnum::Status, ResourceOrderDirectionEnum::Asc);
+
+ Component::factory()->for($group, 'group')->create(['name' => 'OutageSvc', 'status' => ComponentStatusEnum::major_outage]);
+ Component::factory()->for($group, 'group')->create(['name' => 'OkaySvc', 'status' => ComponentStatusEnum::operational]);
+
+ $this->get(route('cachet.status-page'))
+ ->assertOk()
+ ->assertSeeInOrder(['OkaySvc', 'OutageSvc']);
+});
+
+it('falls back to the manual order column when ordering manually', function () {
+ $group = orderingGroup(ResourceOrderColumnEnum::Manual, null);
+
+ Component::factory()->for($group, 'group')->create(['name' => 'ThirdSvc', 'order' => 2]);
+ Component::factory()->for($group, 'group')->create(['name' => 'FirstSvc', 'order' => 0]);
+ Component::factory()->for($group, 'group')->create(['name' => 'SecondSvc', 'order' => 1]);
+
+ $this->get(route('cachet.status-page'))
+ ->assertOk()
+ ->assertSeeInOrder(['FirstSvc', 'SecondSvc', 'ThirdSvc']);
+});