Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
34ee3ef
Add advanced filtering UI for incident list
spalmurray Feb 27, 2026
b8660fc
Fix import paths in AdvancedFilters to use relative imports
spalmurray Feb 27, 2026
98b5fa1
Create filters layout
spalmurray Mar 6, 2026
6348eab
Add filter panel with severity, service tier, and tag filters
spalmurray Mar 11, 2026
b21eaf3
Defer filter param updates until edit close, add All status tab
spalmurray Mar 11, 2026
a585969
Add captain and reporter filters with server-side user search
spalmurray Mar 11, 2026
56c350c
Add date range filter with responsive grid layout
spalmurray Mar 11, 2026
6122efb
Refactor filter components
spalmurray Mar 11, 2026
4606bce
Reset all params instead of explicitly setting everything to undefined
spalmurray Mar 11, 2026
9f21fd4
Use IncidentStatus type instead of string[]
spalmurray Mar 11, 2026
0c5b0de
Add some filter tests
spalmurray Mar 11, 2026
48617da
Fix space key hijacking search input, remove unused MultiSelect
spalmurray Mar 11, 2026
1da7d03
Pull shared editor code out to a hook
spalmurray Mar 12, 2026
c80bc6c
Use ref to access draft state in close callback
spalmurray Apr 9, 2026
23f8f86
Remove unused exports from useActiveFilters
spalmurray Apr 9, 2026
d6da9db
Separate StatusFilterValue from IncidentStatus
spalmurray Apr 9, 2026
19df7d2
Improve advanced filter UX: always-visible X buttons, click-to-edit, …
spalmurray Apr 9, 2026
374d403
Fix pencil icon visibility delay when opening filter edit mode
spalmurray Apr 9, 2026
919a823
Refactor DateRangeFilter to use Tag component with inline close buttons
spalmurray Apr 9, 2026
a4b6bc0
Stabilize useFilterEditor callbacks and remove dead code in useActive…
spalmurray Apr 9, 2026
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
44 changes: 44 additions & 0 deletions frontend/src/routes/components/AdvancedFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {Card} from 'components/Card';

import {ServiceTierSchema, SeveritySchema} from '../types';

import {DateRangeFilter} from './filters/DateRangeFilter';
import {PillFilter} from './filters/PillFilter';
import {TagFilter} from './filters/TagFilter';
import {UserFilter} from './filters/UserFilter';

export {FilterTrigger} from './filters/FilterTrigger';

export function FilterPanel() {
return (
<Card className="flex flex-col gap-space-md" data-testid="advanced-filters">
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-space-md">
<PillFilter
label="Severity"
filterKey="severity"
options={SeveritySchema.options}
/>
<PillFilter
label="Service Tier"
filterKey="service_tier"
options={ServiceTierSchema.options}
/>
<TagFilter label="Impact Type" filterKey="impact_type" tagType="IMPACT_TYPE" />
<TagFilter
label="Affected Service"
filterKey="affected_service"
tagType="AFFECTED_SERVICE"
/>
<TagFilter
label="Affected Region"
filterKey="affected_region"
tagType="AFFECTED_REGION"
/>
<TagFilter label="Root Cause" filterKey="root_cause" tagType="ROOT_CAUSE" />
<UserFilter label="Captain" filterKey="captain" />
<UserFilter label="Reporter" filterKey="reporter" />
<DateRangeFilter />
</div>
</Card>
);
}
4 changes: 2 additions & 2 deletions frontend/src/routes/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Link, useRouterState} from '@tanstack/react-router';
import {Avatar} from 'components/Avatar';

import {currentUserQueryOptions} from '../queries/currentUserQueryOptions';
import type {IncidentStatus} from '../types';
import type {StatusFilterValue} from '../types';

const STORAGE_KEY = 'firetower_list_search';

Expand All @@ -13,7 +13,7 @@ export const Header = () => {

const isRootRoute = routerState.location.pathname === '/';

const getPreservedSearch = (): {status?: IncidentStatus[]} | undefined => {
const getPreservedSearch = (): {status?: StatusFilterValue[]} | undefined => {
const stored = sessionStorage.getItem(STORAGE_KEY);
if (stored) {
try {
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/routes/components/StatusFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import {Link, useSearch} from '@tanstack/react-router';
import {arraysEqual} from 'utils/arrays';
import {cn} from 'utils/cn';

import {STATUS_FILTER_GROUPS, type IncidentStatus} from '../types';
import {STATUS_FILTER_GROUPS, type StatusFilterValue} from '../types';

interface FilterLinkProps {
statuses?: IncidentStatus[];
statuses?: StatusFilterValue[];
label: string;
isActive: boolean;
testId?: string;
Expand Down Expand Up @@ -58,6 +58,12 @@ export function StatusFilter() {
isActive={arraysEqual(status ?? [], STATUS_FILTER_GROUPS.closed)}
testId="filter-closed"
/>
<FilterLink
statuses={STATUS_FILTER_GROUPS.all}
label="All"
isActive={arraysEqual(status ?? [], STATUS_FILTER_GROUPS.all)}
testId="filter-all"
/>
</div>
);
}
154 changes: 154 additions & 0 deletions frontend/src/routes/components/filters/DateRangeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {useState} from 'react';
import {useNavigate} from '@tanstack/react-router';
import {Button} from 'components/Button';
import {Calendar} from 'components/Calendar';
import {Popover, PopoverContent, PopoverTrigger} from 'components/Popover';
import {Tag} from 'components/Tag';
import {XIcon} from 'lucide-react';

import {useActiveFilters} from '../useActiveFilters';

function formatDateDisplay(dateStr: string | undefined): string {
if (!dateStr) return '';
const date = new Date(dateStr.includes('T') ? dateStr : dateStr + 'T00:00:00');
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}

function toDateString(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}

export function DateRangeFilter() {
const navigate = useNavigate();
const {search} = useActiveFilters();
const after = search.created_after as string | undefined;
const before = search.created_before as string | undefined;
const [editing, setEditing] = useState<'after' | 'before' | null>(null);

const afterDate = after ? new Date(after + 'T00:00:00') : undefined;
const beforeDate = before ? new Date(before + 'T00:00:00') : undefined;

const update = (key: 'created_after' | 'created_before', value: string | undefined) => {
navigate({
to: '/',
search: prev => ({...prev, [key]: value}),
replace: true,
});
};

const handleDateSelect = (
key: 'created_after' | 'created_before',
date: Date | undefined
) => {
update(key, date ? toDateString(date) : undefined);
setEditing(null);
};

return (
<div className="col-span-1">
<div className="mb-space-md">
<h3 className="text-size-md text-content-secondary font-semibold">
Created Date
</h3>
</div>
<div className="gap-space-xs flex flex-wrap items-center">
<Popover
open={editing === 'after'}
onOpenChange={o => setEditing(o ? 'after' : null)}
>
<PopoverTrigger asChild>
{after ? (
<Tag
className="cursor-pointer select-none"
action={
<Button
variant="close"
size={null}
onClick={e => {
e.stopPropagation();
update('created_after', undefined);
}}
aria-label="Clear start date"
>
<XIcon className="h-3.5 w-3.5" />
</Button>
}
>
{formatDateDisplay(after)}
</Tag>
) : (
<button
type="button"
className="text-size-sm text-content-disabled cursor-pointer select-none italic"
>
Any
</button>
)}
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={afterDate}
defaultMonth={afterDate}
captionLayout="dropdown"
showOutsideDays={false}
onSelect={d => handleDateSelect('created_after', d)}
/>
</PopoverContent>
</Popover>
<span className="text-content-disabled text-size-sm">to</span>
<Popover
open={editing === 'before'}
onOpenChange={o => setEditing(o ? 'before' : null)}
>
<PopoverTrigger asChild>
{before ? (
<Tag
className="cursor-pointer select-none"
action={
<Button
variant="close"
size={null}
onClick={e => {
e.stopPropagation();
update('created_before', undefined);
}}
aria-label="Clear end date"
>
<XIcon className="h-3.5 w-3.5" />
</Button>
}
>
{formatDateDisplay(before)}
</Tag>
) : (
<button
type="button"
className="text-size-sm text-content-disabled cursor-pointer select-none italic"
>
Any
</button>
)}
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={beforeDate}
defaultMonth={beforeDate}
captionLayout="dropdown"
showOutsideDays={false}
onSelect={d => handleDateSelect('created_before', d)}
/>
</PopoverContent>
</Popover>
</div>
</div>
);
}
46 changes: 46 additions & 0 deletions frontend/src/routes/components/filters/FilterTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {useNavigate} from '@tanstack/react-router';
import {Button} from 'components/Button';
import {SlidersHorizontalIcon} from 'lucide-react';

import {useActiveFilters} from '../useActiveFilters';

export function FilterTrigger({open, onToggle}: {open: boolean; onToggle: () => void}) {
const navigate = useNavigate();
const {activeCount} = useActiveFilters();

return (
<div className="flex items-center gap-space-md">
{activeCount > 0 && (
<button
type="button"
className="text-content-accent text-size-sm cursor-pointer hover:underline"
onClick={() => {
navigate({
to: '/',
search: {},
replace: true,
});
}}
Comment on lines +19 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Clicking "Clear all filters" incorrectly removes the status URL parameter, resetting the user's selected status tab to the default.
Severity: MEDIUM

Suggested Fix

Update the onClick handler in FilterTrigger.tsx to preserve existing URL search parameters when clearing filters. Instead of search: {}, use a functional update like search: s => ({ ...s, ...clearedFilters }) to only remove the relevant filter keys, preserving others like status.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: frontend/src/routes/components/filters/FilterTrigger.tsx#L19-L23

Potential issue: The "Clear all filters" button in `FilterTrigger.tsx` clears all URL
search parameters by calling `navigate` with `search: {}`. When a user is on a
non-default status tab, such as "In Review", the `status` is stored in the URL. If the
user applies advanced filters and then clicks "Clear all filters", this action will
remove not only the advanced filter parameters but also the `status` parameter, causing
the view to unexpectedly reset to the default "Active" status tab.

data-testid="clear-all-filters"
>
Clear all filters
</button>
)}
<Button
variant="secondary"
size="sm"
onClick={onToggle}
aria-expanded={open}
data-testid="advanced-filters-toggle"
>
<SlidersHorizontalIcon className="h-3.5 w-3.5" />
{open ? 'Hide filters' : 'Show filters'}
{activeCount > 0 && (
<span className="bg-background-accent-vibrant text-content-on-vibrant-light ml-space-2xs inline-flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-xs leading-none">
{activeCount}
</span>
)}
</Button>
</div>
);
}
Loading
Loading