diff --git a/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php b/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php new file mode 100644 index 000000000..86e4e2040 --- /dev/null +++ b/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php @@ -0,0 +1,34 @@ +<?php + +namespace BookStack\Actions\Queries; + +use BookStack\Actions\Webhook; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * Get all the webhooks in the system in a paginated format. + */ +class WebhooksAllPaginatedAndSorted +{ + /** + * @param array{sort: string, order: string, search: string} $sortData + */ + public function run(int $count, array $sortData): LengthAwarePaginator + { + $sort = $sortData['sort']; + + $query = Webhook::query()->select(['*']) + ->withCount(['trackedEvents']) + ->orderBy($sort, $sortData['order']); + + if ($sortData['search']) { + $term = '%' . $sortData['search'] . '%'; + $query->where(function ($query) use ($term) { + $query->where('name', 'like', $term) + ->orWhere('endpoint', 'like', $term); + }); + } + + return $query->paginate($count); + } +} diff --git a/app/Auth/Queries/AllRolesPaginatedAndSorted.php b/app/Auth/Queries/RolesAllPaginatedAndSorted.php similarity index 96% rename from app/Auth/Queries/AllRolesPaginatedAndSorted.php rename to app/Auth/Queries/RolesAllPaginatedAndSorted.php index add1e9e54..6abbfd1ad 100644 --- a/app/Auth/Queries/AllRolesPaginatedAndSorted.php +++ b/app/Auth/Queries/RolesAllPaginatedAndSorted.php @@ -8,7 +8,7 @@ use Illuminate\Pagination\LengthAwarePaginator; /** * Get all the roles in the system in a paginated format. */ -class AllRolesPaginatedAndSorted +class RolesAllPaginatedAndSorted { /** * @param array{sort: string, order: string, search: string} $sortData diff --git a/app/Auth/Queries/AllUsersPaginatedAndSorted.php b/app/Auth/Queries/UsersAllPaginatedAndSorted.php similarity index 97% rename from app/Auth/Queries/AllUsersPaginatedAndSorted.php rename to app/Auth/Queries/UsersAllPaginatedAndSorted.php index 29e58fe09..3a64cc800 100644 --- a/app/Auth/Queries/AllUsersPaginatedAndSorted.php +++ b/app/Auth/Queries/UsersAllPaginatedAndSorted.php @@ -11,7 +11,7 @@ use Illuminate\Pagination\LengthAwarePaginator; * user is assumed to be trusted. (Admin users). * Email search can be abused to extract email addresses. */ -class AllUsersPaginatedAndSorted +class UsersAllPaginatedAndSorted { /** * @param array{sort: string, order: string, search: string} $sortData diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index d022bf35d..0e13a9fb3 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -3,7 +3,7 @@ namespace BookStack\Http\Controllers; use BookStack\Auth\Permissions\PermissionsRepo; -use BookStack\Auth\Queries\AllRolesPaginatedAndSorted; +use BookStack\Auth\Queries\RolesAllPaginatedAndSorted; use BookStack\Auth\Role; use BookStack\Exceptions\PermissionsException; use Exception; @@ -32,7 +32,7 @@ class RoleController extends Controller 'order' => setting()->getForCurrentUser('roles_sort_order', 'asc'), ]; - $roles = (new AllRolesPaginatedAndSorted())->run(20, $listDetails); + $roles = (new RolesAllPaginatedAndSorted())->run(20, $listDetails); $roles->appends(['search' => $listDetails['search']]); $this->setPageTitle(trans('settings.roles')); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index bd69aa8f5..77be07533 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,7 +3,7 @@ namespace BookStack\Http\Controllers; use BookStack\Auth\Access\SocialAuthService; -use BookStack\Auth\Queries\AllUsersPaginatedAndSorted; +use BookStack\Auth\Queries\UsersAllPaginatedAndSorted; use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Auth\UserRepo; @@ -42,7 +42,7 @@ class UserController extends Controller 'order' => setting()->getForCurrentUser('users_sort_order', 'asc'), ]; - $users = (new AllUsersPaginatedAndSorted())->run(20, $listDetails); + $users = (new UsersAllPaginatedAndSorted())->run(20, $listDetails); $this->setPageTitle(trans('settings.users')); $users->appends(['search' => $listDetails['search']]); @@ -251,7 +251,7 @@ class UserController extends Controller */ public function changeSort(Request $request, string $id, string $type) { - $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles']; + $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks']; if (!in_array($type, $validSortTypes)) { return redirect()->back(500); } @@ -322,7 +322,7 @@ class UserController extends Controller // Probably better to do a simple validation here then validate at usage. $validSorts = [ 'name', 'created_at', 'updated_at', 'default', 'email', 'last_activity_at', 'display_name', - 'users_count', 'permissions_count', + 'users_count', 'permissions_count', 'endpoint', 'active', ]; if (!in_array($sort, $validSorts)) { $sort = 'name'; diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index 264921dfc..23120c7e4 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\ActivityType; +use BookStack\Actions\Queries\WebhooksAllPaginatedAndSorted; use BookStack\Actions\Webhook; use Illuminate\Http\Request; @@ -18,16 +19,23 @@ class WebhookController extends Controller /** * Show all webhooks configured in the system. */ - public function index() + public function index(Request $request) { - $webhooks = Webhook::query() - ->orderBy('name', 'desc') - ->with('trackedEvents') - ->get(); + $listDetails = [ + 'search' => $request->get('search', ''), + 'sort' => setting()->getForCurrentUser('webhooks_sort', 'name'), + 'order' => setting()->getForCurrentUser('webhooks_sort_order', 'asc'), + ]; + + $webhooks = (new WebhooksAllPaginatedAndSorted())->run(20, $listDetails); + $webhooks->appends(['search' => $listDetails['search']]); $this->setPageTitle(trans('settings.webhooks')); - return view('settings.webhooks.index', ['webhooks' => $webhooks]); + return view('settings.webhooks.index', [ + 'webhooks' => $webhooks, + 'listDetails' => $listDetails, + ]); } /** diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index e8978d41e..f4204dd68 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -247,6 +247,8 @@ return [ // Webhooks 'webhooks' => 'Webhooks', + 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', + 'webhooks_x_trigger_events' => '1 trigger event|:count trigger events', 'webhooks_create' => 'Create New Webhook', 'webhooks_none_created' => 'No webhooks have yet been created.', 'webhooks_edit' => 'Edit Webhook', diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 667c26388..acb45100f 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -969,4 +969,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } .item-list-row:hover .item-list-row-toggle-all { visibility: visible; +} + +.status-indicator-active, .status-indicator-inactive { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} +.status-indicator-active { + background-color: $positive; +} +.status-indicator-inactive { + background-color: $negative; } \ No newline at end of file diff --git a/resources/views/common/status-indicator.blade.php b/resources/views/common/status-indicator.blade.php new file mode 100644 index 000000000..ba9b1b463 --- /dev/null +++ b/resources/views/common/status-indicator.blade.php @@ -0,0 +1,3 @@ +<span title="{{ trans('common.status_' . ($status ? 'active' : 'inactive')) }}" + class="status-indicator-{{ $status ? 'active' : 'inactive' }}" +></span> \ No newline at end of file diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php index 6aeb16f92..7e3d5b852 100644 --- a/resources/views/settings/roles/index.blade.php +++ b/resources/views/settings/roles/index.blade.php @@ -31,6 +31,8 @@ 'display_name' => trans('common.sort_name'), 'users_count' => trans('settings.roles_assigned_users'), 'permissions_count' => trans('settings.roles_permissions_provided'), + 'created_at' => trans('common.sort_created_at'), + 'updated_at' => trans('common.sort_updated_at'), ], 'order' => $listDetails['order'], 'sort' => $listDetails['sort'], 'type' => 'roles']) </div> </div> diff --git a/resources/views/settings/webhooks/index.blade.php b/resources/views/settings/webhooks/index.blade.php index bbe58453f..09b2ee770 100644 --- a/resources/views/settings/webhooks/index.blade.php +++ b/resources/views/settings/webhooks/index.blade.php @@ -8,48 +8,51 @@ <div class="card content-wrap auto-height"> - <div class="grid half v-center"> + <div class="flex-container-row items-center justify-space-between wrap"> <h1 class="list-heading">{{ trans('settings.webhooks') }}</h1> - <div class="text-right"> + <div> <a href="{{ url("/settings/webhooks/create") }}" class="button outline">{{ trans('settings.webhooks_create') }}</a> </div> </div> - @if(count($webhooks) > 0) + <p class="text-muted">{{ trans('settings.webhooks_index_desc') }}</p> - <table class="table"> - <tr> - <th>{{ trans('common.name') }}</th> - <th width="100">{{ trans('settings.webhook_events_table_header') }}</th> - <th width="100">{{ trans('common.status') }}</th> - </tr> + <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap"> + <div> + <div class="block inline mr-xs"> + <form method="get" action="{{ url("/settings/webhooks") }}"> + <input type="text" name="search" placeholder="{{ trans('common.search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif> + </form> + </div> + </div> + <div class="justify-flex-end"> + @include('common.sort', ['options' => [ + 'name' => trans('common.sort_name'), + 'endpoint' => trans('settings.webhooks_endpoint'), + 'created_at' => trans('common.sort_created_at'), + 'updated_at' => trans('common.sort_updated_at'), + 'active' => trans('common.status'), + ], 'order' => $listDetails['order'], 'sort' => $listDetails['sort'], 'type' => 'webhooks']) + </div> + </div> + + @if(count($webhooks) > 0) + <div class="item-list"> @foreach($webhooks as $webhook) - <tr> - <td> - <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> <br> - <span class="small text-muted italic">{{ $webhook->endpoint }}</span> - </td> - <td> - @if($webhook->tracksEvent('all')) - {{ trans('settings.webhooks_events_all') }} - @else - {{ $webhook->trackedEvents->count() }} - @endif - </td> - <td> - {{ trans('common.status_' . ($webhook->active ? 'active' : 'inactive')) }} - </td> - </tr> + @include('settings.webhooks.parts.webhooks-list-item', ['webhook' => $webhook]) @endforeach - </table> + </div> @else <p class="text-muted empty-text px-none"> {{ trans('settings.webhooks_none_created') }} </p> @endif + <div class="my-m"> + {{ $webhooks->links() }} + </div> </div> </div> diff --git a/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php b/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php new file mode 100644 index 000000000..5b7d135eb --- /dev/null +++ b/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php @@ -0,0 +1,18 @@ +<div class="item-list-row py-s"> + <div class="flex-container-row"> + <div class="flex-2 py-xxs px-m flex-container-row items-center gap-s"> + @include('common.status-indicator', ['status' => $webhook->active]) + <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> + </div> + <div class="flex py-xxs px-m text-right text-muted"> + @if($webhook->tracksEvent('all')) + {{ trans('settings.webhooks_events_all') }} + @else + {{ trans_choice('settings.webhooks_x_trigger_events', $webhook->tracked_events_count, ['count' => $webhook->tracked_events_count]) }} + @endif + </div> + </div> + <div class="px-m py-xxs text-muted italic text-limit-lines-1"> + <small>{{ $webhook->endpoint }}</small> + </div> +</div> \ No newline at end of file