Skip to content

Commit d7dfbe3

Browse files
wesmclaude
andcommitted
feat: centralized active filter badges in dashboard toolbar
Move filter badges from individual charts (ProjectBreakdown, HourOfWeekHeatmap) to a new ActiveFilters bar below the toolbar. Shows clickable chips for date, project, and time filters with clear buttons, plus a "Clear all" when multiple filters are active. Individual charts retain their visual highlighting (dimming, selected state) but no longer show their own badge/clear UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 075e273 commit d7dfbe3

File tree

5 files changed

+246
-90
lines changed

5 files changed

+246
-90
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<script lang="ts">
2+
import { analytics } from "../../stores/analytics.svelte.js";
3+
4+
const DAY_LABELS = [
5+
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
6+
];
7+
8+
const dateLabel = $derived.by(() => {
9+
if (!analytics.selectedDate) return "";
10+
const d = new Date(analytics.selectedDate + "T00:00:00");
11+
return d.toLocaleDateString("en", {
12+
month: "short",
13+
day: "numeric",
14+
year: "numeric",
15+
});
16+
});
17+
18+
const timeLabel = $derived.by(() => {
19+
const dow = analytics.selectedDow;
20+
const hour = analytics.selectedHour;
21+
if (dow !== null && hour !== null) {
22+
return `${DAY_LABELS[dow]} ${String(hour).padStart(2, "0")}:00`;
23+
}
24+
if (dow !== null) return DAY_LABELS[dow]!;
25+
if (hour !== null) {
26+
return `${String(hour).padStart(2, "0")}:00`;
27+
}
28+
return "";
29+
});
30+
31+
const hasTime = $derived(
32+
analytics.selectedDow !== null ||
33+
analytics.selectedHour !== null,
34+
);
35+
36+
const filterCount = $derived(
37+
(analytics.selectedDate !== null ? 1 : 0) +
38+
(analytics.project !== "" ? 1 : 0) +
39+
(hasTime ? 1 : 0)
40+
);
41+
</script>
42+
43+
{#if analytics.hasActiveFilters}
44+
<div class="active-filters">
45+
<span class="filters-label">Filters:</span>
46+
47+
{#if analytics.selectedDate}
48+
<button
49+
class="filter-chip"
50+
onclick={() => analytics.clearDate()}
51+
title="Clear date filter"
52+
>
53+
<span class="chip-icon">
54+
<svg width="10" height="10" viewBox="0 0 16 16"
55+
fill="currentColor">
56+
<path d="M4.5 1a.5.5 0 01.5.5V2h6v-.5a.5.5
57+
0 011 0V2h1a2 2 0 012 2v9a2 2 0 01-2
58+
2H3a2 2 0 01-2-2V4a2 2 0 012-2h1v-.5a.5.5
59+
0 01.5-.5zM3 6v7a1 1 0 001 1h8a1 1 0
60+
001-1V6H3z"/>
61+
</svg>
62+
</span>
63+
{dateLabel}
64+
<span class="chip-x">&times;</span>
65+
</button>
66+
{/if}
67+
68+
{#if analytics.project}
69+
<button
70+
class="filter-chip"
71+
onclick={() => analytics.clearProject()}
72+
title="Clear project filter"
73+
>
74+
<span class="chip-icon">
75+
<svg width="10" height="10" viewBox="0 0 16 16"
76+
fill="currentColor">
77+
<path d="M1 3.5A1.5 1.5 0 012.5 2h2.764a1.5
78+
1.5 0 011.025.404l.961.878A.5.5 0
79+
007.59 3.5H13.5A1.5 1.5 0 0115 5v7.5a1.5
80+
1.5 0 01-1.5 1.5h-11A1.5 1.5 0 011
81+
12.5v-9z"/>
82+
</svg>
83+
</span>
84+
{analytics.project}
85+
<span class="chip-x">&times;</span>
86+
</button>
87+
{/if}
88+
89+
{#if hasTime}
90+
<button
91+
class="filter-chip"
92+
onclick={() => analytics.clearTimeFilter()}
93+
title="Clear time filter"
94+
>
95+
<span class="chip-icon">
96+
<svg width="10" height="10" viewBox="0 0 16 16"
97+
fill="currentColor">
98+
<path d="M8 1a7 7 0 100 14A7 7 0 008 1zm.5
99+
3a.5.5 0 00-1 0v4a.5.5 0
100+
00.146.354l2 2a.5.5 0 00.708-.708L8.5
101+
7.793V4z"/>
102+
</svg>
103+
</span>
104+
{timeLabel}
105+
<span class="chip-x">&times;</span>
106+
</button>
107+
{/if}
108+
109+
{#if filterCount > 1}
110+
<button
111+
class="clear-all"
112+
onclick={() => analytics.clearAllFilters()}
113+
title="Clear all filters"
114+
>
115+
Clear all
116+
</button>
117+
{/if}
118+
</div>
119+
{/if}
120+
121+
<style>
122+
.active-filters {
123+
display: flex;
124+
align-items: center;
125+
gap: 6px;
126+
padding: 4px 16px 6px;
127+
background: var(--bg-surface);
128+
border-bottom: 1px solid var(--border-muted);
129+
flex-shrink: 0;
130+
flex-wrap: wrap;
131+
}
132+
133+
.filters-label {
134+
font-size: 10px;
135+
font-weight: 500;
136+
color: var(--text-muted);
137+
text-transform: uppercase;
138+
letter-spacing: 0.03em;
139+
}
140+
141+
.filter-chip {
142+
display: inline-flex;
143+
align-items: center;
144+
gap: 4px;
145+
height: 22px;
146+
padding: 0 6px;
147+
font-size: 11px;
148+
font-weight: 500;
149+
color: var(--accent-blue);
150+
background: color-mix(
151+
in srgb, var(--accent-blue) 10%, transparent
152+
);
153+
border-radius: var(--radius-sm);
154+
cursor: pointer;
155+
transition: background 0.1s;
156+
}
157+
158+
.filter-chip:hover {
159+
background: color-mix(
160+
in srgb, var(--accent-blue) 18%, transparent
161+
);
162+
}
163+
164+
.chip-icon {
165+
display: flex;
166+
align-items: center;
167+
opacity: 0.7;
168+
}
169+
170+
.chip-x {
171+
font-size: 13px;
172+
line-height: 1;
173+
margin-left: 2px;
174+
opacity: 0.6;
175+
}
176+
177+
.filter-chip:hover .chip-x {
178+
opacity: 1;
179+
}
180+
181+
.clear-all {
182+
height: 22px;
183+
padding: 0 8px;
184+
font-size: 10px;
185+
font-weight: 500;
186+
color: var(--text-muted);
187+
border-radius: var(--radius-sm);
188+
cursor: pointer;
189+
transition: background 0.1s, color 0.1s;
190+
}
191+
192+
.clear-all:hover {
193+
background: var(--bg-surface-hover);
194+
color: var(--text-secondary);
195+
}
196+
</style>

frontend/src/lib/components/analytics/AnalyticsPage.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import ToolUsage from "./ToolUsage.svelte";
1212
import AgentComparison from "./AgentComparison.svelte";
1313
import TopSessions from "./TopSessions.svelte";
14+
import ActiveFilters from "./ActiveFilters.svelte";
1415
import { analytics } from "../../stores/analytics.svelte.js";
1516
import { exportAnalyticsCSV } from "../../utils/csv-export.js";
1617
@@ -63,6 +64,8 @@
6364
</button>
6465
</div>
6566

67+
<ActiveFilters />
68+
6669
<div class="analytics-content">
6770
<SummaryCards />
6871

frontend/src/lib/components/analytics/HourOfWeekHeatmap.svelte

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,6 @@
126126
analytics.selectHourOfWeek(null, hour);
127127
}
128128
129-
function clearFilter() {
130-
analytics.selectHourOfWeek(null, null);
131-
}
132-
133129
function isDimmed(dow: number, hour: number): boolean {
134130
const sd = analytics.selectedDow;
135131
const sh = analytics.selectedHour;
@@ -141,24 +137,6 @@
141137
return hour !== sh;
142138
}
143139
144-
const hasFilter = $derived(
145-
analytics.selectedDow !== null ||
146-
analytics.selectedHour !== null,
147-
);
148-
149-
const filterLabel = $derived.by(() => {
150-
const sd = analytics.selectedDow;
151-
const sh = analytics.selectedHour;
152-
if (sd !== null && sh !== null) {
153-
return `${DAY_LABELS[sd]} ${sh.toString().padStart(2, "0")}:00`;
154-
}
155-
if (sd !== null) return DAY_LABELS[sd]!;
156-
if (sh !== null) {
157-
return `${sh.toString().padStart(2, "0")}:00`;
158-
}
159-
return "";
160-
});
161-
162140
function shortTz(tz: string): string {
163141
const slash = tz.lastIndexOf("/");
164142
return slash >= 0
@@ -173,16 +151,6 @@
173151
Activity by Day and Hour
174152
<span class="tz-label">{shortTz(analytics.timezone)}</span>
175153
</h3>
176-
{#if hasFilter}
177-
<span class="filter-badge">
178-
{filterLabel}
179-
<button
180-
class="clear-btn"
181-
onclick={clearFilter}
182-
aria-label="Clear time filter"
183-
>&times;</button>
184-
</span>
185-
{/if}
186154
</div>
187155

188156
{#if analytics.loading.hourOfWeek}
@@ -295,30 +263,6 @@
295263
margin-left: 4px;
296264
}
297265
298-
.filter-badge {
299-
display: inline-flex;
300-
align-items: center;
301-
gap: 4px;
302-
padding: 1px 6px;
303-
font-size: 10px;
304-
font-weight: 500;
305-
color: var(--accent-blue);
306-
background: var(--user-bg);
307-
border-radius: var(--radius-sm);
308-
}
309-
310-
.clear-btn {
311-
font-size: 12px;
312-
line-height: 1;
313-
color: var(--text-muted);
314-
cursor: pointer;
315-
padding: 0 2px;
316-
}
317-
318-
.clear-btn:hover {
319-
color: var(--text-primary);
320-
}
321-
322266
.how-scroll {
323267
overflow-x: auto;
324268
padding-bottom: 4px;

frontend/src/lib/components/analytics/ProjectBreakdown.svelte

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,7 @@
9090
<div class="breakdown-container">
9191
<div class="breakdown-header">
9292
<h3 class="chart-title">Projects</h3>
93-
{#if analytics.project}
94-
<button
95-
class="filter-badge"
96-
onclick={() => analytics.setProject(analytics.project)}
97-
>
98-
{analytics.project}
99-
<span class="clear-x">&times;</span>
100-
</button>
101-
{:else if rows.length > 0}
93+
{#if rows.length > 0}
10294
<span class="count">{analytics.projects?.projects.length ?? 0} total</span>
10395
{/if}
10496
</div>
@@ -219,31 +211,6 @@
219211
opacity: 0.7;
220212
}
221213
222-
.filter-badge {
223-
display: flex;
224-
align-items: center;
225-
gap: 4px;
226-
padding: 1px 6px;
227-
font-size: 10px;
228-
color: var(--accent-blue);
229-
background: color-mix(
230-
in srgb, var(--accent-blue) 12%, transparent
231-
);
232-
border-radius: var(--radius-sm);
233-
cursor: pointer;
234-
}
235-
236-
.filter-badge:hover {
237-
background: color-mix(
238-
in srgb, var(--accent-blue) 20%, transparent
239-
);
240-
}
241-
242-
.clear-x {
243-
font-size: 12px;
244-
line-height: 1;
245-
}
246-
247214
.project-name {
248215
flex-shrink: 0;
249216
width: 140px;

frontend/src/lib/stores/analytics.svelte.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,52 @@ class AnalyticsStore {
116116
return Intl.DateTimeFormat().resolvedOptions().timeZone;
117117
}
118118

119+
get hasActiveFilters(): boolean {
120+
return (
121+
this.selectedDate !== null ||
122+
this.project !== "" ||
123+
this.selectedDow !== null ||
124+
this.selectedHour !== null
125+
);
126+
}
127+
128+
clearAllFilters() {
129+
this.selectedDate = null;
130+
this.project = "";
131+
this.selectedDow = null;
132+
this.selectedHour = null;
133+
this.fetchAll();
134+
}
135+
136+
clearDate() {
137+
this.selectedDate = null;
138+
this.fetchSummary();
139+
this.fetchActivity();
140+
this.fetchProjects();
141+
this.fetchSessionShape();
142+
this.fetchVelocity();
143+
this.fetchTools();
144+
this.fetchTopSessions();
145+
}
146+
147+
clearProject() {
148+
this.project = "";
149+
this.fetchAll();
150+
}
151+
152+
clearTimeFilter() {
153+
this.selectedDow = null;
154+
this.selectedHour = null;
155+
this.fetchSummary();
156+
this.fetchActivity();
157+
this.fetchHeatmap();
158+
this.fetchProjects();
159+
this.fetchSessionShape();
160+
this.fetchVelocity();
161+
this.fetchTools();
162+
this.fetchTopSessions();
163+
}
164+
119165
private baseParams(
120166
opts: {
121167
includeProject?: boolean;

0 commit comments

Comments
 (0)