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
2 changes: 2 additions & 0 deletions .claude/skills/create-dashboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ filters:

**Filter types:** `select`, `date-range`, `date`, `number`, `text`

**Searchable selects:** both single and multiple `select` filters show a searchable dropdown, so you can type to find an option quickly when the list is long.

**Date range presets:** `today`, `yesterday`, `last_7_days`, `last_30_days`, `last_90_days`, `this_month`, `last_month`, `this_quarter`, `this_year`, `year_to_date`, `all_time`. If `options.presets` is omitted, a default set is shown. Users can always pick "Custom range" for arbitrary dates.

---
Expand Down
2 changes: 2 additions & 0 deletions cmd/skill_templates/create-dashboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ Supported filter types:

Date range presets include `today`, `yesterday`, `last_7_days`, `last_30_days`, `last_90_days`, `this_month`, `last_month`, `this_quarter`, `this_year`, `year_to_date`, and `all_time`.

Both single and multiple `select` filters show a searchable dropdown, so you can type to find an option quickly when the list is long.

Select filters support `multiple: true` for multi-select. The value is a list — render with `join` in Jinja and guard the empty case:

```sql
Expand Down
76 changes: 54 additions & 22 deletions frontend/src/themes/bruin/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
}

/** Resolve a preset key string to {start, end}. Returns null if not a known preset. */
export function resolvePreset(key: string): { start: string; end: string } | null {

Check warning on line 166 in frontend/src/themes/bruin/FilterBar.tsx

View workflow job for this annotation

GitHub Actions / verify

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const p = ALL_PRESETS.find((pr) => pr.key === key);
return p ? p.resolve() : null;
}
Expand Down Expand Up @@ -197,22 +197,7 @@
if (filter.multiple) {
return <MultiSelectFilter filter={filter} value={value} onChange={onChange} label={label} />;
}
return (
<div className="flex flex-col gap-1">
<label className="text-[10px] font-medium uppercase tracking-wider text-[var(--dac-text-muted)]">
{label}
</label>
<select
className={inputClass}
value={String(value ?? filter.default ?? "")}
onChange={(e) => onChange(e.target.value)}
>
{filter.options?.values?.map((v) => (
<option key={v} value={v}>{v}</option>
))}
</select>
</div>
);
return <SingleSelectFilter filter={filter} value={value} onChange={onChange} label={label} />;

case "date-range":
return <DateRangeFilter filter={filter} value={value} onChange={onChange} label={label} />;
Expand Down Expand Up @@ -438,6 +423,50 @@
value: string;
}

function SingleSelectFilter({
filter,
value,
onChange,
label,
}: {
filter: Filter;
value: unknown;
onChange: (value: unknown) => void;
label: string;
}) {
const options: Option[] = (filter.options?.values ?? []).map((v) => ({ label: v, value: v }));
const current = value ?? filter.default;
const selected = current != null && current !== ""
? options.find((o) => o.value === String(current)) ?? null
: null;
const inputId = `select-${filter.name}`;

return (
<div className="flex flex-col gap-1">
<label
htmlFor={inputId}
className="text-[10px] font-medium uppercase tracking-wider text-[var(--dac-text-muted)]"
>
{label}
</label>
<Select<Option, false>
inputId={inputId}
aria-label={label}
options={options}
value={selected}
onChange={(opt) => onChange(opt ? opt.value : "")}
placeholder="Select..."
isSearchable
menuPortalTarget={popoverPortalTarget() as HTMLElement}
menuPosition="fixed"
unstyled
classNames={selectClassNames}
styles={selectStyles}
/>
</div>
);
}

function MultiSelectFilter({
filter,
value,
Expand Down Expand Up @@ -467,7 +496,7 @@
inputId={inputId}
aria-label={label}
isMulti
isSearchable={false}
isSearchable
options={options}
value={selected}
onChange={(opts) => onChange(opts.map((o) => o.value))}
Expand All @@ -488,16 +517,20 @@

function CompactValueContainer({ children, ...props }: ValueContainerProps<Option, true>) {
const count = props.getValue().length;
const isTyping = Boolean(props.selectProps.inputValue);
if (count === 0) {
return <components.ValueContainer {...props}>{children}</components.ValueContainer>;
}
const childArray = children as React.ReactNode[];
const input = childArray[childArray.length - 1];
return (
<components.ValueContainer {...props}>
<span className="text-[13px] text-[var(--dac-text-primary)] truncate">
{count === 1 ? props.getValue()[0].label : `${count} selected`}
</span>
{/* Hide the "N selected" summary while the user is typing a search. */}
{!isTyping && (
<span className="text-[13px] text-[var(--dac-text-primary)] truncate">
{count === 1 ? props.getValue()[0].label : `${count} selected`}
</span>
)}
{input}
</components.ValueContainer>
);
Expand All @@ -513,6 +546,7 @@
}`,
valueContainer: () => "overflow-hidden",
placeholder: () => "text-[var(--dac-text-muted)]",
singleValue: () => "text-[var(--dac-text-primary)] truncate",
input: () => "text-[var(--dac-text-primary)]",
indicatorsContainer: () => "text-[var(--dac-text-muted)]",
indicatorSeparator: () => "hidden",
Expand Down Expand Up @@ -555,8 +589,6 @@
...base,
margin: 0,
padding: 0,
height: 0,
width: 0,
}),
indicatorsContainer: (base: Record<string, unknown>) => ({
...base,
Expand Down
Loading