Skip to content
32 changes: 25 additions & 7 deletions front/components/pages/workspace/AnalyticsConsumptionPage.tsx
Comment thread
avervaet marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { ConsumptionAttributionTable } from "@app/components/workspace/analytics/consumption/ConsumptionAttributionTable";
import { ConsumptionOverview } from "@app/components/workspace/analytics/consumption/ConsumptionOverview";
import { ConsumptionPeriodSelector } from "@app/components/workspace/analytics/consumption/ConsumptionPeriodSelector";
import type { ConsumptionDimension } from "@app/components/workspace/analytics/consumption/consumptionDimensions";
import { DEFAULT_CONSUMPTION_DIMENSION } from "@app/components/workspace/analytics/consumption/consumptionDimensions";
import type { ConsumptionPeriodSelection } from "@app/lib/analytics/consumption_period";
import { DEFAULT_CONSUMPTION_PERIOD } from "@app/lib/analytics/consumption_period";
import { UsageFilterPanel } from "@app/components/workspace/analytics/UsageFilterPanel";
import type { UsageFilter } from "@app/components/workspace/analytics/usageFilter";
import {
USAGE_FILTER_MOCK_GROUPS,
USAGE_FILTER_MOCK_OPTIONS,
} from "@app/components/workspace/analytics/usageFilterMockData";

Check failure on line 13 in front/components/pages/workspace/AnalyticsConsumptionPage.tsx

View workflow job for this annotation

GitHub Actions / format-check

assist/source/organizeImports

Sort these imports.

@avervaet avervaet Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed higher in the stack

import { useFeatureFlags, useWorkspace } from "@app/lib/auth/AuthContext";
import { isNavigationLocked } from "@app/lib/navigation-lock";
import { BarChart01, cn, Page, SafeSuspense, safeLazy } from "@dust-tt/sparkle";
Expand Down Expand Up @@ -37,6 +43,7 @@
const [dimension, setDimension] = useState<ConsumptionDimension>(
DEFAULT_CONSUMPTION_DIMENSION
);
const [filter, setFilter] = useState<UsageFilter>({});

if (!isEnabled) {
return (
Expand Down Expand Up @@ -75,13 +82,24 @@
/>
<div className="flex flex-col gap-8 pb-8">
<ConsumptionOverview workspaceId={owner.sId} period={period} />
<SafeSuspense fallback={<ChartFallback />}>
<ConsumptionChart
workspaceId={owner.sId}
period={period}
dimension={dimension}
/>
</SafeSuspense>
<div className="flex flex-col gap-2">
<div className="flex justify-end">
<UsageFilterPanel
owner={owner}
categoryOptions={USAGE_FILTER_MOCK_OPTIONS}
groups={USAGE_FILTER_MOCK_GROUPS}
filter={filter}
onFilterChange={setFilter}
/>
</div>
<SafeSuspense fallback={<ChartFallback />}>
<ConsumptionChart
workspaceId={owner.sId}
period={period}
dimension={dimension}
/>
</SafeSuspense>
</div>
<ConsumptionAttributionTable
workspaceId={owner.sId}
period={period}
Expand Down
265 changes: 265 additions & 0 deletions front/components/workspace/analytics/UsageFilterPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import type {
UsageFilter,
UsageFilterAgentOption,
UsageFilterCategory,
UsageFilterGroup,
UsageFilterMemberOption,
UsageFilterModelOption,
UsageFilterOptionForCategory,
UsageFilterScope,
UsageFilterSkillOption,
UsageFilterSourceOption,
UsageFilterToolOption,
UsageModelTier,
} from "@app/components/workspace/analytics/usageFilter";
import {
USAGE_FILTER_CATEGORIES,
USAGE_FILTER_CATEGORY_LABEL,
USAGE_FILTER_SCOPES,
USAGE_MODEL_TIERS,
} from "@app/components/workspace/analytics/usageFilter";
import { UsageFilterAgentScopeControls } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterAgentScopeControls";
import { UsageFilterCategoryNav } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterCategoryNav";
import { UsageFilterFooter } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterFooter";
import { UsageFilterMemberGroupsControls } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterMemberGroupsControls";
import { UsageFilterModelComplexityControls } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterModelComplexityControls";
import { UsageFilterOptionCheckboxList } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterOptionCheckboxList";
import { UsageFilterSelectionSummary } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterSelectionSummary";
import { useUsageFilter } from "@app/components/workspace/analytics/useUsageFilter";
import { useSearchMembers } from "@app/lib/swr/memberships";
import type { LightWorkspaceType } from "@app/types/user";
import {
BarChart05,
Button,
NavigationListLabel,
PopoverContent,
PopoverRoot,
PopoverTrigger,
SearchInput,
} from "@dust-tt/sparkle";
import { useMemo, useState } from "react";

interface UsageFilterPanelProps {
owner: LightWorkspaceType;
categoryOptions: {
agent: UsageFilterAgentOption[];
model: UsageFilterModelOption[];
tool: UsageFilterToolOption[];
skill: UsageFilterSkillOption[];
source: UsageFilterSourceOption[];
};
groups: UsageFilterGroup[];
filter: UsageFilter;
onFilterChange: (next: UsageFilter) => void;
}

export function UsageFilterPanel({
owner,
categoryOptions,
groups,
filter,
onFilterChange,
}: UsageFilterPanelProps) {
const [isOpen, setIsOpen] = useState(false);
// Selections are staged while the panel is open and only propagated
// when the user clicks Apply.
const {
draftFilter,
setDraftFilter,
clearAllCategories,
clearCategory,
toggleOption,
removeOption,
selectAllFiltered,
} = useUsageFilter(filter);
const [activeCategory, setActiveCategory] =
useState<UsageFilterCategory>("agent");
const [activeScope, setActiveScope] = useState<UsageFilterScope>(
USAGE_FILTER_SCOPES[0]
);
const [activeTier, setActiveTier] = useState<UsageModelTier>(
USAGE_MODEL_TIERS[0]
);
const [searchText, setSearchText] = useState("");

const { members } = useSearchMembers({
workspaceId: owner.sId,
searchTerm: activeCategory === "member" ? searchText : "",
pageIndex: 0,
pageSize: 100,
disabled: !isOpen || activeCategory !== "member",
});

const memberOptions = useMemo<UsageFilterMemberOption[]>(
() =>
members.map((m) => ({
id: m.sId,
name: m.fullName,
kind: "member",
image: m.image,
})),
[members]
);

const resolvedCategoryOptions = useMemo<{
[C in UsageFilterCategory]: UsageFilterOptionForCategory<C>[];
}>(
() => ({
...categoryOptions,
member: memberOptions,
}),
[categoryOptions, memberOptions]
);

const activeOptions = resolvedCategoryOptions[activeCategory];
const filteredOptions = useMemo(() => {
const search = searchText.trim().toLowerCase();
return activeOptions.filter((option) => {
if (option.kind === "agent" && option.scope !== activeScope) {
return false;
}
if (option.kind === "model" && option.tier !== activeTier) {
return false;
}
if (search && !option.name.toLowerCase().includes(search)) {
return false;
}
return true;
});
}, [activeOptions, searchText, activeScope, activeTier]);

const selectedIdsForActiveCategory = useMemo(
() =>
new Set((draftFilter[activeCategory] ?? []).map((option) => option.id)),
[draftFilter, activeCategory]
);

const appliedSelectionCount = useMemo(
() =>
USAGE_FILTER_CATEGORIES.reduce(
(count, category) => count + (filter[category]?.length ?? 0),
0
),
[filter]
);

const categoriesWithSelection = useMemo(
() =>
USAGE_FILTER_CATEGORIES.filter(
(category) => (draftFilter[category]?.length ?? 0) > 0
),
[draftFilter]
);

const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (open) {
setDraftFilter(filter);
setSearchText("");
}
};

const handleCategoryChange = (category: UsageFilterCategory) => {
setActiveCategory(category);
setSearchText("");
};

const handleCancel = () => {
setIsOpen(false);
};

const handleApply = () => {
onFilterChange(draftFilter);
setIsOpen(false);
};

const activeCategorySelectionCount = draftFilter[activeCategory]?.length ?? 0;

return (
<PopoverRoot open={isOpen} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
icon={BarChart05}
label="Filters"
size="sm"
variant="outline"
isCounter={appliedSelectionCount > 0}
counterValue={String(appliedSelectionCount)}
/>
</PopoverTrigger>
<PopoverContent fullWidth align="end" className="w-auto rounded-2xl p-0">
<div className="flex h-96 flex-row divide-x divide-border">
<UsageFilterCategoryNav
categories={USAGE_FILTER_CATEGORIES}
draftFilter={draftFilter}
activeCategory={activeCategory}
onCategoryChange={handleCategoryChange}
/>
<div className="flex h-full w-72 flex-col gap-2 p-2">
<NavigationListLabel
label={USAGE_FILTER_CATEGORY_LABEL[activeCategory]}
className="bg-transparent pt-1.5 pb-0 font-medium"
action={
<Button
label="Clear"
size="xmini"
variant="ghost-secondary"
onClick={() => clearCategory(activeCategory)}
disabled={activeCategorySelectionCount === 0}
className={
activeCategorySelectionCount === 0 ? "invisible" : undefined
}
/>
}
/>
<SearchInput
name="usage-filter-search"
value={searchText}
onChange={setSearchText}
placeholder={`Search ${USAGE_FILTER_CATEGORY_LABEL[activeCategory].toLowerCase()}`}
/>
{activeCategory === "member" && (
<UsageFilterMemberGroupsControls groups={groups} />
)}
{activeCategory === "model" && (
<UsageFilterModelComplexityControls
models={categoryOptions.model}
selectedModelIds={selectedIdsForActiveCategory}
onToggleModel={(model) => toggleOption("model", model)}
activeTier={activeTier}
onTierChange={setActiveTier}
/>
)}
{activeCategory === "agent" && (
<UsageFilterAgentScopeControls
activeScope={activeScope}
onScopeChange={setActiveScope}
/>
)}
<UsageFilterOptionCheckboxList
category={activeCategory}
categoryLabel={USAGE_FILTER_CATEGORY_LABEL[activeCategory]}
options={filteredOptions}
selectedIds={selectedIdsForActiveCategory}
onToggleOption={(option) => toggleOption(activeCategory, option)}
onSelectAll={() =>
selectAllFiltered(activeCategory, filteredOptions)
}
/>
</div>
<UsageFilterSelectionSummary
categoriesWithSelection={categoriesWithSelection}
draftFilter={draftFilter}
onClearCategory={clearCategory}
onRemoveOption={removeOption}
/>
</div>
<UsageFilterFooter
onClearAll={clearAllCategories}
onCancel={handleCancel}
onApply={handleApply}
/>
</PopoverContent>
</PopoverRoot>
);
}
Loading
Loading