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