Skip to content

Commit

Permalink
perf(sessions): improve filterOptions api (#2337)
Browse files Browse the repository at this point in the history
* perf(sessions): improve `filterOptions` api

* limit to 1000 results

* fix: remove unintentional line from `traces.filterOptions`

* feat(table_filters): add free text search option

* push

* show custom select option in `userIds` column on sessions table

* style: fix padding

* style: improve styling

* simplify

* push

* feat(multi-select): show `customSelect` also when "no results found"
  • Loading branch information
marliessophie committed Jun 17, 2024
1 parent 42e446e commit f2152dd
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 25 deletions.
3 changes: 3 additions & 0 deletions web/src/components/table/data-table-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface DataTableToolbarProps<TData, TValue> {
setColumnVisibility?: Dispatch<SetStateAction<VisibilityState>>;
rowHeight?: RowHeight;
setRowHeight?: Dispatch<SetStateAction<RowHeight>>;
columnsWithCustomSelect?: string[];
}

export function DataTableToolbar<TData, TValue>({
Expand All @@ -44,6 +45,7 @@ export function DataTableToolbar<TData, TValue>({
setColumnVisibility,
rowHeight,
setRowHeight,
columnsWithCustomSelect,
}: DataTableToolbarProps<TData, TValue>) {
const [searchString, setSearchString] = useState(
searchConfig?.currentQuery ?? "",
Expand Down Expand Up @@ -84,6 +86,7 @@ export function DataTableToolbar<TData, TValue>({
columns={filterColumnDefinition}
filterState={filterState}
onChange={setFilterState}
columnsWithCustomSelect={columnsWithCustomSelect}
/>
)}
<div className="flex flex-row flex-wrap gap-2 pr-0.5 @6xl:ml-auto">
Expand Down
1 change: 1 addition & 0 deletions web/src/components/table/use-cases/sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export default function SessionsTable({
columns={columns}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
columnsWithCustomSelect={["userIds"]}
/>
<DataTable
columns={columns}
Expand Down
1 change: 0 additions & 1 deletion web/src/ee/features/evals/components/eval-config-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ export const InnerEvalConfigForm = (props: {

const traceFilterOptions = api.traces.filterOptions.useQuery({
projectId: props.projectId,
...form.getFieldState("filter"),
});

useEffect(() => {
Expand Down
9 changes: 9 additions & 0 deletions web/src/features/filters/components/filter-builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export function PopoverFilterBuilder({
columns,
filterState,
onChange,
columnsWithCustomSelect = [],
}: {
columns: ColumnDefinition[];
filterState: FilterState;
onChange: Dispatch<SetStateAction<FilterState>>;
columnsWithCustomSelect?: string[];
}) {
const capture = usePostHogClientCapture();
const [wipFilterState, _setWipFilterState] =
Expand Down Expand Up @@ -118,6 +120,7 @@ export function PopoverFilterBuilder({
columns={columns}
filterState={wipFilterState}
onChange={setWipFilterState}
columnsWithCustomSelect={columnsWithCustomSelect}
/>
</PopoverContent>
</Popover>
Expand Down Expand Up @@ -211,11 +214,13 @@ function FilterBuilderForm({
filterState,
onChange,
disabled,
columnsWithCustomSelect = [],
}: {
columns: ColumnDefinition[];
filterState: WipFilterState;
onChange: Dispatch<SetStateAction<WipFilterState>>;
disabled?: boolean;
columnsWithCustomSelect?: string[];
}) {
const handleFilterChange = (filter: WipFilterCondition, i: number) => {
onChange((prev) => {
Expand Down Expand Up @@ -419,6 +424,10 @@ function FilterBuilderForm({
}
values={Array.isArray(filter.value) ? filter.value : []}
disabled={disabled}
isCustomSelectEnabled={
column?.type === filter.type &&
columnsWithCustomSelect.includes(column.id)
}
/>
) : filter.type === "boolean" ? (
<Select
Expand Down
134 changes: 122 additions & 12 deletions web/src/features/filters/components/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ import {
} from "@/src/components/ui/popover";
import { Separator } from "@/src/components/ui/separator";
import { type FilterOption } from "@langfuse/shared";
import { Input } from "@/src/components/ui/input";
import { useRef, useState } from "react";

const getFreeTextInput = (
isCustomSelectEnabled: boolean,
values: string[],
optionValues: Set<string>,
): string | undefined =>
isCustomSelectEnabled
? Array.from(values.values()).find((value) => !optionValues.has(value))
: undefined;

export function MultiSelect({
title,
Expand All @@ -28,15 +39,54 @@ export function MultiSelect({
options,
className,
disabled,
isCustomSelectEnabled = false,
}: {
title?: string;
values: string[];
onValueChange: (values: string[]) => void;
options: FilterOption[] | readonly FilterOption[];
className?: string;
disabled?: boolean;
isCustomSelectEnabled?: boolean;
}) {
const selectedValues = new Set(values);
const optionValues = new Set(options.map((option) => option.value));
const freeTextInput = getFreeTextInput(
isCustomSelectEnabled,
values,
optionValues,
);
const [freeText, setFreeText] = useState(freeTextInput || "");

const debounceTimeout = useRef<NodeJS.Timeout | null>(null);
const handleDebouncedChange = (value: string) => {
const freeTextInput = getFreeTextInput(
isCustomSelectEnabled,
values,
optionValues,
);

if (!!freeTextInput) {
selectedValues.delete(freeTextInput);
selectedValues.add(value);
selectedValues.delete("");
const filterValues = Array.from(selectedValues);
onValueChange(filterValues.length ? filterValues : []);
}
};

function getSelectedOptions() {
const selectedOptions = options.filter(({ value }) =>
selectedValues.has(value),
);

const hasCustomOption =
!!freeText &&
!!getFreeTextInput(isCustomSelectEnabled, values, optionValues);
const customOption = hasCustomOption ? [{ value: freeText }] : [];

return [...selectedOptions, ...customOption];
}

return (
<Popover>
Expand Down Expand Up @@ -69,17 +119,15 @@ export function MultiSelect({
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.value}
</Badge>
))
getSelectedOptions().map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.value}
</Badge>
))
)}
</div>
</>
Expand All @@ -90,7 +138,10 @@ export function MultiSelect({
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{/* if isCustomSelectEnabled we always show custom select hence never empty */}
{!isCustomSelectEnabled && (
<CommandEmpty>No results found.</CommandEmpty>
)}
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value);
Expand Down Expand Up @@ -127,6 +178,65 @@ export function MultiSelect({
);
})}
</CommandGroup>
{isCustomSelectEnabled && (
<CommandGroup forceMount={true}>
<CommandSeparator />
<CommandItem
key="freeTextField"
onSelect={() => {
const freeTextInput = getFreeTextInput(
isCustomSelectEnabled,
values,
optionValues,
);

if (!!freeTextInput) {
selectedValues.delete(freeTextInput);
} else {
selectedValues.add(freeText);
}
selectedValues.delete("");
const filterValues = Array.from(selectedValues);
onValueChange(filterValues.length ? filterValues : []);
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
getFreeTextInput(
isCustomSelectEnabled,
values,
optionValues,
) ||
(optionValues.has(freeText) &&
selectedValues.has(freeText))
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<Check className="h-4 w-4" />
</div>
<Input
type="text"
value={freeText}
onChange={(e) => {
setFreeText(e.target.value);
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
debounceTimeout.current = setTimeout(() => {
handleDebouncedChange(e.target.value);
}, 500);
}}
onClick={(e) => {
e.stopPropagation();
}}
placeholder="Enter custom value"
className="h-6 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-dotted p-0 text-sm"
/>
</CommandItem>
</CommandGroup>
)}
{selectedValues.size > 0 && (
<>
<CommandSeparator />
Expand Down
39 changes: 27 additions & 12 deletions web/src/server/api/routers/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,33 @@ export const sessionRouter = createTRPCRouter({
}),
)
.query(async ({ input, ctx }) => {
const userIds: { value: string; count: number }[] = await ctx.prisma
.$queryRaw`
SELECT traces.user_id as value, COUNT(traces.user_id)::int as count
FROM traces
WHERE traces.session_id IS NOT NULL
AND traces.project_id = ${input.projectId}
GROUP BY traces.user_id;
`;
const res: SessionOptions = {
userIds: userIds,
};
return res;
try {
const userIds = await ctx.prisma.$queryRaw<
Array<{ value: string }>
>(Prisma.sql`
SELECT
traces.user_id AS value
FROM traces
WHERE
traces.session_id IS NOT NULL
AND traces.user_id IS NOT NULL
AND traces.project_id = ${input.projectId}
GROUP BY
traces.user_id
LIMIT 1000;
`);

const res: SessionOptions = {
userIds: userIds,
};
return res;
} catch (e) {
console.error(e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "unable to get session filter options",
});
}
}),
byId: protectedGetSessionProcedure
.input(z.object({ projectId: z.string(), sessionId: z.string() }))
Expand Down

0 comments on commit f2152dd

Please sign in to comment.