Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions app/Http/Controllers/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,26 @@ public function index(Request $request): Response

public function endpoints(Request $request): Response
{
$endpoints = $request->user()->endpoints()
->with('events')
->paginate(15);
$query = $request->user()->endpoints()->with('events');

if ($request->filled('search')) {
$search = $request->string('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('url', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}

if ($request->filled('status')) {
$query->where('is_active', $request->string('status') === 'active');
}

$endpoints = $query->paginate(15)->withQueryString();

return Inertia::render('Endpoints/Index', [
'endpoints' => $endpoints,
'filters' => $request->only(['search', 'status']),
]);
}

Expand Down
42 changes: 17 additions & 25 deletions resources/js/Pages/Endpoints/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@
v-model="search"
placeholder="Search by name, URL, or description"
class="w-full pl-9"
@keyup.enter="applyFilters"
/>
</div>
<div class="flex flex-col gap-3 sm:flex-row">
<select v-model="statusFilter" class="app-select min-w-40">
<select v-model="statusFilter" class="app-select min-w-40" @change="applyFilters">
<option value="">All statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
Expand Down Expand Up @@ -312,10 +313,11 @@ import Pagination from '@/Components/Pagination.vue'

const props = defineProps({
endpoints: Object,
filters: Object,
})

const search = ref('')
const statusFilter = ref('')
const search = ref(props.filters?.search || '')
const statusFilter = ref(props.filters?.status || '')
const showCreateModal = ref(false)
const showEditModal = ref(false)
const showTestModal = ref(false)
Expand All @@ -333,28 +335,7 @@ const form = useForm({
is_active: true,
})

const filteredEndpoints = computed(() => {
let filtered = props.endpoints.data || []

if (search.value) {
const searchLower = search.value.toLowerCase()
filtered = filtered.filter(endpoint =>
endpoint.name.toLowerCase().includes(searchLower) ||
endpoint.url.toLowerCase().includes(searchLower) ||
(endpoint.description && endpoint.description.toLowerCase().includes(searchLower))
)
}

if (statusFilter.value) {
filtered = filtered.filter(endpoint => {
if (statusFilter.value === 'active') return endpoint.is_active
if (statusFilter.value === 'inactive') return !endpoint.is_active
return true
})
}

return filtered
})
const filteredEndpoints = computed(() => props.endpoints.data || [])

const endpointStats = computed(() => {
const all = props.endpoints.data || []
Expand Down Expand Up @@ -477,9 +458,20 @@ async function regenerateSecret() {
}
}

function applyFilters() {
router.get(route('endpoints'), {
search: search.value,
status: statusFilter.value,
}, {
preserveState: true,
replace: true,
})
}

function clearFilters() {
search.value = ''
statusFilter.value = ''
applyFilters()
}

function formatDate(dateString) {
Expand Down
73 changes: 73 additions & 0 deletions tests/Feature/DashboardEndpointsSearchFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Tests\Feature;

use App\Models\Endpoint;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class DashboardEndpointsSearchFilterTest extends TestCase
{
use RefreshDatabase;

public function test_search_finds_a_matching_endpoint_beyond_the_first_page(): void
{
$user = User::factory()->withPersonalTeam()->create();

// Fill more than a full page (15) with endpoints that don't match the search term.
for ($i = 0; $i < 15; $i++) {
Endpoint::factory()->for($user)->create(['name' => "Noise Endpoint {$i}"]);
}
$target = Endpoint::factory()->for($user)->create(['name' => 'Unique Billing Receiver']);

$response = $this->actingAs($user)->get(route('endpoints', ['search' => 'Unique Billing Receiver']));

$response->assertOk();
$response->assertInertia(
fn ($page) => $page
->component('Endpoints/Index')
->has('endpoints.data', 1)
->where('endpoints.data.0.id', $target->id)
->where('filters.search', 'Unique Billing Receiver')
);
}

public function test_status_filter_finds_a_matching_endpoint_beyond_the_first_page(): void
{
$user = User::factory()->withPersonalTeam()->create();

for ($i = 0; $i < 15; $i++) {
Endpoint::factory()->for($user)->create(['is_active' => true]);
}
$target = Endpoint::factory()->for($user)->create(['is_active' => false]);

$response = $this->actingAs($user)->get(route('endpoints', ['status' => 'inactive']));

$response->assertOk();
$response->assertInertia(
fn ($page) => $page
->component('Endpoints/Index')
->has('endpoints.data', 1)
->where('endpoints.data.0.id', $target->id)
->where('filters.status', 'inactive')
);
}

public function test_search_cannot_be_used_to_view_another_users_endpoints(): void
{
$user = User::factory()->withPersonalTeam()->create();

$otherUser = User::factory()->withPersonalTeam()->create();
Endpoint::factory()->for($otherUser)->create(['name' => 'Other Users Endpoint']);

$response = $this->actingAs($user)->get(route('endpoints', ['search' => 'Other Users Endpoint']));

$response->assertOk();
$response->assertInertia(
fn ($page) => $page
->component('Endpoints/Index')
->has('endpoints.data', 0)
);
}
}
Loading