feat(tables): filter tables by clicking a cell value

Click a type, status, server, source, or role value in a table row to
filter the table by that value. Click it again to remove the filter.
Covers the environment resources, deployments, application and service
backups, and team members tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-29 10:53:44 +01:00
co-authored by Claude Opus 5.5
parent a3e2b9347c
commit cd992e72e1
6 changed files with 105 additions and 18 deletions
+32
View File
@@ -3070,6 +3070,38 @@ input[type="search"]::-webkit-search-results-decoration {
background: rgba(255, 255, 255, 0.02);
}
/* A table cell value that filters the table by that value on click. */
.table-filter-value {
position: relative;
z-index: 1;
max-width: 100%;
pointer-events: auto;
cursor: pointer;
text-align: left;
text-decoration-line: underline;
text-decoration-color: transparent;
text-underline-offset: 2px;
transition: color 0.15s, border-color 0.15s, text-decoration-color 0.15s;
}
.table-filter-value:hover {
color: var(--color-black);
text-decoration-color: currentColor;
}
.dark .table-filter-value:hover {
color: var(--color-white);
}
.table-filter-value.table-filter-badge:hover {
text-decoration-color: transparent;
border-color: var(--color-neutral-400);
}
.dark .table-filter-value.table-filter-badge:hover {
border-color: rgba(255, 255, 255, 0.3);
}
.env-table-grid {
grid-template-columns: minmax(14rem, 2.5fr) 4.8rem 6rem 4rem 4.5rem 4.8rem 4.2rem 3rem;
}
@@ -205,7 +205,12 @@
title="{{ $backup->targetName() }}">
{{ $backup->targetName() }}
</span>
<span>{{ $backup->targetType() }}</span>
<span class="min-w-0">
<span role="button" tabindex="0" class="table-filter-value"
title="Filter by {{ $backup->targetType() }}"
x-on:click.prevent.stop="typeFilter = typeFilter === @js(strtolower($backup->targetType())) ? 'all' : @js(strtolower($backup->targetType()))"
x-on:keydown.enter.prevent.stop="$el.click()">{{ $backup->targetType() }}</span>
</span>
<span>{{ $backup->frequency }}</span>
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
<span title="{{ $backup->save_s3 ? ($s3StorageNames->isNotEmpty() ? $s3StorageNames->join(', ') : 'S3 storage unavailable') : 'S3 storage is not configured' }}">
@@ -236,6 +236,15 @@
(bool) data_get($deployment, 'is_api') => 'API',
default => 'Manual',
};
$sourceKey = match (true) {
$pullRequestId > 0 => 'pull-request',
(bool) data_get($deployment, 'is_webhook') => 'webhook',
(bool) data_get($deployment, 'rollback') => 'rollback',
(bool) data_get($deployment, 'is_api') => 'api',
default => 'manual',
};
$serverId = data_get($deployment, 'server_id');
$serverName = data_get($deployment, 'server_name') ?: data_get($application, 'destination.server.name', '-');
$duration = match ($deploymentStatus) {
'queued' => 'Waiting',
'in_progress' => calculateDuration(data_get($deployment, 'created_at'), now()),
@@ -264,9 +273,21 @@
x-bind:aria-busy="selecting === '{{ data_get($deployment, 'deployment_uuid') }}'"
@endif>
@unless ($embedded)
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!}><x-status-badge :status="$statusLabel" :type="$statusType" /></a>
<span class="min-w-0">
<x-status-badge as="button" :status="$statusLabel" :type="$statusType"
class="table-filter-value table-filter-badge" title="Filter by {{ $statusLabel }}"
wire:click="toggleDeploymentFilter('status:{{ $deploymentStatus }}')" />
</span>
@endunless
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!}>{{ $sourceLabel }}</a>
@if ($embedded)
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!}>{{ $sourceLabel }}</a>
@else
<span class="min-w-0">
<button type="button" class="table-filter-value truncate"
title="Filter by {{ $sourceLabel }}"
wire:click="toggleDeploymentFilter('source:{{ $sourceKey }}')">{{ $sourceLabel }}</button>
</span>
@endif
<span class="min-w-0">
@if (data_get($deployment, 'commit'))
<span class="flex min-w-0 items-center gap-2">
@@ -285,7 +306,7 @@
@endif
@if ($embedded)
<span class="mt-0.5 hidden truncate text-xs text-neutral-500 group-hover:text-neutral-950 dark:text-fg-faint dark:group-hover:text-white 2xl:block">
{{ $sourceLabel }} · <span class="tabular-nums">{{ $duration }}</span> · {{ data_get($deployment, 'server_name') ?: data_get($application, 'destination.server.name', '-') }}
{{ $sourceLabel }} · <span class="tabular-nums">{{ $duration }}</span> · {{ $serverName }}
</span>
@endif
</span>
@@ -295,9 +316,15 @@
</a>
@endunless
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!} class="tabular-nums">{{ $duration }}</a>
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!} class="truncate">
{{ data_get($deployment, 'server_name') ?: data_get($application, 'destination.server.name', '-') }}
</a>
@if (! $embedded && $serverId)
<span class="min-w-0">
<button type="button" class="table-filter-value truncate"
title="Filter by {{ $serverName }}"
wire:click="toggleDeploymentFilter('server:{{ (int) $serverId }}')">{{ $serverName }}</button>
</span>
@else
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!} class="truncate">{{ $serverName }}</a>
@endif
@if ($embedded)
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!}
class="flex items-center justify-end gap-2 pr-1 tabular-nums"
@@ -206,11 +206,17 @@
</div>
</div>
<div class="resource-type truncate text-[12px] text-neutral-600 dark:text-fg-dim"
x-text="item.typeLabel"></div>
<div class="resource-type min-w-0">
<button type="button"
class="table-filter-value truncate text-[12px] text-neutral-600 dark:text-fg-dim"
x-on:click="toggleFilter('typeFilters', item.type)"
x-bind:title="`Filter by ${item.typeLabel}`" x-text="item.typeLabel"></button>
</div>
<div>
<x-status-badge dynamic x-bind:title="statusTitle(item)">
<div class="min-w-0">
<x-status-badge dynamic as="button" class="table-filter-value table-filter-badge"
x-on:click="toggleFilter('statusFilters', statusState(item))"
x-bind:title="`${statusTitle(item)} · Filter by ${statusLabel(item)}`">
<span class="size-1.5 shrink-0 rounded-full"
x-bind:class="statusDotClass(item)"></span>
<span class="truncate" x-text="statusLabel(item)"></span>
@@ -226,8 +232,13 @@
<span x-show="!item.fqdn" class="text-[12px] text-neutral-400 dark:text-fg-faint">-</span>
</div>
<div class="resource-server truncate text-[12px] text-neutral-600 dark:text-fg-dim"
x-text="item.destination?.server?.name || 'Unknown'"></div>
<div class="resource-server min-w-0">
<button type="button"
class="table-filter-value truncate text-[12px] text-neutral-600 dark:text-fg-dim"
x-on:click="toggleFilter('serverFilters', item.destination?.server?.name || 'Unknown')"
x-bind:title="`Filter by ${item.destination?.server?.name || 'Unknown'}`"
x-text="item.destination?.server?.name || 'Unknown'"></button>
</div>
<div class="resource-tags flex min-w-0 items-center gap-1 overflow-hidden">
<template x-for="tag in item.tags.slice(0, 2)" :key="tag.id">
@@ -279,7 +279,12 @@
<span class="min-w-0 truncate font-medium text-neutral-950 dark:text-fg">
{{ $databaseBackup->database->human_name ?: $databaseBackup->database->name }}
</span>
<span>Database</span>
<span class="min-w-0">
<button type="button" class="table-filter-value"
title="Filter by Database"
x-on:click.stop="typeFilter = typeFilter === 'database' ? 'all' : 'database'"
x-on:keydown.enter.stop>Database</button>
</span>
<span>{{ $databaseBackup->frequency }}</span>
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
<span>
@@ -333,7 +338,12 @@
title="{{ $backup->targetName() }}">
{{ $backup->targetName() }}
</span>
<span>{{ $backup->targetType() }}</span>
<span class="min-w-0">
<button type="button" class="table-filter-value"
title="Filter by {{ $backup->targetType() }}"
x-on:click.stop="typeFilter = typeFilter === @js(strtolower($backup->targetType())) ? 'all' : @js(strtolower($backup->targetType()))"
x-on:keydown.enter.stop>{{ $backup->targetType() }}</button>
</span>
<span>{{ $backup->frequency }}</span>
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
<span>
@@ -27,10 +27,12 @@
</div>
<div class="truncate text-[12px] text-neutral-500 dark:text-fg-dim">{{ $member->email }}</div>
<div>
<span
class="inline-flex rounded-full bg-neutral-100 px-2 py-0.5 text-[10px] font-medium capitalize text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim">
<button type="button"
class="table-filter-value table-filter-badge inline-flex rounded-full border border-transparent bg-neutral-100 px-2 py-0.5 text-[10px] font-medium capitalize text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim"
title="Filter by {{ data_get($member, 'pivot.role') }}"
x-on:click="roleFilter = roleFilter === @js(data_get($member, 'pivot.role')) ? 'all' : @js(data_get($member, 'pivot.role')); page = 1">
{{ data_get($member, 'pivot.role') }}
</span>
</button>
</div>
@can('manageMembers', currentTeam())
<div class="flex items-center">