Skip to content

Commit ea09a67

Browse files
committed
icons
1 parent c5e5ce2 commit ea09a67

File tree

4 files changed

+285
-13
lines changed

4 files changed

+285
-13
lines changed

dashboard/ai-analytics/src/app/components/BarList.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@
33
import { useState } from 'react';
44
import { RiSearchLine } from '@remixicon/react';
55
import { BarList as TremorBarList, Card, Dialog, DialogPanel, TextInput } from '@tremor/react';
6+
import { LucideIcon } from 'lucide-react';
67

78
interface BarListItem {
89
name: string;
910
value: number;
11+
icon?: React.ReactNode;
1012
}
1113

1214
interface BarListProps {
1315
data: Array<{
1416
name: string;
1517
value: number;
18+
icon?: React.ReactNode;
1619
}>;
1720
valueFormatter?: (value: number) => string;
1821
onSelectionChange?: (selectedItems: string[]) => void;
22+
categoryType?: string; // Add category type to determine icons
1923
}
2024

2125
const defaultFormatter = (number: number) =>
@@ -24,7 +28,8 @@ const defaultFormatter = (number: number) =>
2428
export default function BarList({
2529
data,
2630
valueFormatter = defaultFormatter,
27-
onSelectionChange
31+
onSelectionChange,
32+
categoryType
2833
}: BarListProps) {
2934
const [isOpen, setIsOpen] = useState(false);
3035
const [searchQuery, setSearchQuery] = useState('');
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
'use client';
2+
3+
import { Card } from '@tremor/react';
4+
import { useState } from 'react';
5+
import { RiSearchLine } from '@remixicon/react';
6+
import { Dialog, DialogPanel, TextInput } from '@tremor/react';
7+
8+
interface BarListItem {
9+
name: string;
10+
value: number;
11+
icon?: React.ReactNode;
12+
}
13+
14+
interface CustomBarListProps {
15+
data: BarListItem[];
16+
valueFormatter?: (value: number) => string;
17+
onSelectionChange?: (selectedItems: string[]) => void;
18+
}
19+
20+
const defaultFormatter = (number: number) =>
21+
`${Intl.NumberFormat('us').format(number).toString()}`;
22+
23+
export default function CustomBarList({
24+
data,
25+
valueFormatter = defaultFormatter,
26+
onSelectionChange
27+
}: CustomBarListProps) {
28+
const [isOpen, setIsOpen] = useState(false);
29+
const [searchQuery, setSearchQuery] = useState('');
30+
const [selectedItems, setSelectedItems] = useState<string[]>([]);
31+
32+
const filteredItems = data.filter((item) =>
33+
item.name.toLowerCase().includes(searchQuery.toLowerCase()),
34+
);
35+
36+
// Calculate total value for header
37+
const totalValue = data.reduce((sum, item) => sum + item.value, 0);
38+
const hasMoreItems = data.length > 5;
39+
40+
const handleBarClick = (itemName: string) => {
41+
setSelectedItems(prev => {
42+
const newSelection = prev.includes(itemName)
43+
? prev.filter(name => name !== itemName)
44+
: [...prev, itemName];
45+
46+
onSelectionChange?.(newSelection);
47+
return newSelection;
48+
});
49+
};
50+
51+
// Custom bar rendering with icons
52+
const renderCustomBarList = (items: BarListItem[]) => (
53+
<div className="mt-4 space-y-3">
54+
{items.map((item) => {
55+
// Calculate percentage for bar width (max 92% to leave room for text)
56+
const maxValue = Math.max(...items.map(i => i.value));
57+
const percentage = maxValue > 0 ? (item.value / maxValue) * 92 : 0;
58+
59+
return (
60+
<div
61+
key={item.name}
62+
className="flex items-center cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 p-1 rounded transition-colors"
63+
onClick={() => handleBarClick(item.name)}
64+
>
65+
<div className="flex items-center w-full">
66+
<div className="flex items-center min-w-0 flex-1">
67+
{item.icon && (
68+
<div className="mr-2 flex-shrink-0">
69+
{item.icon}
70+
</div>
71+
)}
72+
<p className="truncate text-tremor-default text-tremor-content dark:text-dark-tremor-content">
73+
{item.name}
74+
</p>
75+
</div>
76+
<p className="ml-2 flex-shrink-0 text-right text-tremor-default text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis">
77+
{valueFormatter(item.value)}
78+
</p>
79+
</div>
80+
<div className="w-full h-1 mt-1">
81+
<div
82+
className="h-full bg-indigo-500 rounded-full"
83+
style={{ width: `${percentage}%` }}
84+
/>
85+
</div>
86+
</div>
87+
);
88+
})}
89+
</div>
90+
);
91+
92+
return (
93+
<>
94+
<Card className="h-full w-full rounded-none border-0" style={{ boxShadow: '-1px 0 0 0 rgb(55 65 81)' }}>
95+
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
96+
{valueFormatter(totalValue)}
97+
</p>
98+
{renderCustomBarList(data.slice(0, 5))}
99+
{hasMoreItems && (
100+
<div className="absolute inset-x-0 bottom-0 flex justify-center rounded-b-tremor-default bg-gradient-to-t from-tremor-background to-transparent py-7 dark:from-dark-tremor-background">
101+
<button
102+
className="flex items-center justify-center rounded-tremor-small border border-tremor-border bg-tremor-background px-2.5 py-2 text-tremor-default font-medium text-tremor-content-strong shadow-tremor-input hover:bg-tremor-background-muted dark:border-dark-tremor-border dark:bg-dark-tremor-background dark:text-dark-tremor-content-strong dark:shadow-dark-tremor-input hover:dark:bg-dark-tremor-background-muted"
103+
onClick={() => setIsOpen(true)}
104+
>
105+
Show more
106+
</button>
107+
</div>
108+
)}
109+
<Dialog
110+
open={isOpen}
111+
onClose={() => setIsOpen(false)}
112+
static={true}
113+
className="z-[100]"
114+
>
115+
<DialogPanel className="overflow-hidden p-0">
116+
<div className="px-6 pb-4 pt-6">
117+
<TextInput
118+
icon={RiSearchLine}
119+
placeholder="Search..."
120+
className="rounded-tremor-small"
121+
value={searchQuery}
122+
onValueChange={setSearchQuery}
123+
/>
124+
<div className="flex items-center justify-between pt-4">
125+
<p className="text-tremor-default font-medium text-tremor-content-strong dark:text-dark-tremor-content-strong">
126+
Name
127+
</p>
128+
<p className="text-tremor-label font-medium uppercase text-tremor-content dark:text-dark-tremor-content">
129+
Cost
130+
</p>
131+
</div>
132+
</div>
133+
<div className="h-96 overflow-y-scroll px-6">
134+
{filteredItems.length > 0 ? (
135+
renderCustomBarList(filteredItems)
136+
) : (
137+
<p className="flex h-full items-center justify-center text-tremor-default text-tremor-content-strong dark:text-dark-tremor-content-strong">
138+
No results.
139+
</p>
140+
)}
141+
</div>
142+
<div className="mt-4 border-t border-tremor-border bg-tremor-background-muted p-6 dark:border-dark-tremor-border dark:bg-dark-tremor-background">
143+
<button
144+
className="flex w-full items-center justify-center rounded-tremor-small border border-tremor-border bg-tremor-background py-2 text-tremor-default font-medium text-tremor-content-strong shadow-tremor-input hover:bg-tremor-background-muted dark:border-dark-tremor-border dark:bg-dark-tremor-background dark:text-dark-tremor-content-strong dark:shadow-dark-tremor-input hover:dark:bg-dark-tremor-background-muted"
145+
onClick={() => setIsOpen(false)}
146+
>
147+
Go back
148+
</button>
149+
</div>
150+
</DialogPanel>
151+
</Dialog>
152+
</Card>
153+
</>
154+
);
155+
}

dashboard/ai-analytics/src/app/components/TabbedPane.tsx

Lines changed: 123 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,95 @@
33
import { Tab, TabGroup, TabList } from '@tremor/react';
44
import { useGenericCounter } from '@/hooks/useTinybirdData';
55
import { useSearchParams } from 'next/navigation';
6-
import BarList from './BarList';
6+
import CustomBarList from './CustomBarList';
77
import { useState, useEffect } from 'react';
88
import { tabs } from '../constants';
99
import { useTinybirdToken } from '@/providers/TinybirdProvider';
10+
import {
11+
Server,
12+
Cloud,
13+
User,
14+
Building2,
15+
Cpu
16+
} from 'lucide-react';
17+
18+
// Custom OpenAI Icon
19+
const OpenAIIcon = () => (
20+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" className="text-green-500">
21+
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="currentColor"/>
22+
</svg>
23+
);
24+
25+
// Custom Anthropic Icon
26+
const AnthropicIcon = () => (
27+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" className="text-purple-500">
28+
<path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z" fill="currentColor"/>
29+
</svg>
30+
);
31+
32+
// Custom Google AI Icon
33+
const GoogleAIIcon = () => (
34+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" className="text-blue-500">
35+
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="currentColor"/>
36+
</svg>
37+
);
38+
39+
// Custom GPT-4 Icon
40+
const GPT4Icon = () => (
41+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" className="text-green-600">
42+
<path d="M18.5 3h-13A2.5 2.5 0 0 0 3 5.5v13A2.5 2.5 0 0 0 5.5 21h13a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 18.5 3zM8 17.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v11zm5 0a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v11zm5 0a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v11z" fill="currentColor"/>
43+
</svg>
44+
);
45+
46+
// Custom Claude Icon
47+
const ClaudeIcon = () => (
48+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" className="text-purple-600">
49+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-4-8c0-2.21 1.79-4 4-4s4 1.79 4 4-1.79 4-4 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" fill="currentColor"/>
50+
<path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" fill="currentColor"/>
51+
</svg>
52+
);
53+
54+
// Helper function to get icon for provider
55+
const getProviderIcon = (provider: string) => {
56+
const lowerProvider = provider.toLowerCase();
57+
if (lowerProvider.includes('openai')) {
58+
return <OpenAIIcon />;
59+
} else if (lowerProvider.includes('anthropic')) {
60+
return <AnthropicIcon />;
61+
} else if (lowerProvider.includes('google')) {
62+
return <GoogleAIIcon />;
63+
} else {
64+
return <Cloud className="w-4 h-4 text-gray-500" />;
65+
}
66+
};
67+
68+
// Helper function to get icon for model
69+
const getModelIcon = (model: string) => {
70+
const lowerModel = model.toLowerCase();
71+
if (lowerModel.includes('gpt')) {
72+
return <OpenAIIcon />;
73+
} else if (lowerModel.includes('claude')) {
74+
return <AnthropicIcon />;
75+
} else if (lowerModel.includes('palm') || lowerModel.includes('gemini')) {
76+
return <GoogleAIIcon />;
77+
} else {
78+
return <Cpu className="w-4 h-4 text-gray-500" />;
79+
}
80+
};
81+
82+
// Helper function to get icon for environment
83+
const getEnvironmentIcon = (env: string) => {
84+
const lowerEnv = env.toLowerCase();
85+
if (lowerEnv.includes('prod')) {
86+
return <Server className="w-4 h-4 text-green-500" />;
87+
} else if (lowerEnv.includes('staging')) {
88+
return <Server className="w-4 h-4 text-yellow-500" />;
89+
} else if (lowerEnv.includes('dev')) {
90+
return <Server className="w-4 h-4 text-blue-500" />;
91+
} else {
92+
return <Server className="w-4 h-4 text-gray-500" />;
93+
}
94+
};
1095

1196
interface TabbedPaneProps {
1297
filters: Record<string, string>;
@@ -19,7 +104,7 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
19104
const filteredTabs = tabs.filter(tab => !orgName || tab.key !== 'organization');
20105
const initialDimension = searchParams.get('dimension') || filteredTabs[0].key;
21106
const [selectedTab, setSelectedTab] = useState<string>(initialDimension);
22-
const [barListData, setBarListData] = useState<Array<{ name: string; value: number }>>([]);
107+
const [barListData, setBarListData] = useState<Array<{ name: string; value: number; icon?: React.ReactNode }>>([]);
23108
// eslint-disable-next-line @typescript-eslint/no-unused-vars
24109
const [selectedValues, setSelectedValues] = useState<string[]>([]);
25110

@@ -77,13 +162,40 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
77162
useEffect(() => {
78163
if (data?.data) {
79164
// eslint-disable-next-line @typescript-eslint/no-explicit-any
80-
const newData = data.data.map((item: any) => ({
81-
name: item.category || 'Unknown',
82-
value: item.total_cost || 0 // Use total_cost instead of count
83-
}));
165+
const newData = data.data.map((item: any) => {
166+
const name = item.category || 'Unknown';
167+
let icon;
168+
169+
// Assign icon based on the selected tab
170+
switch (selectedTab) {
171+
case 'provider':
172+
icon = getProviderIcon(name);
173+
break;
174+
case 'model':
175+
icon = getModelIcon(name);
176+
break;
177+
case 'environment':
178+
icon = getEnvironmentIcon(name);
179+
break;
180+
case 'organization':
181+
icon = <Building2 className="w-4 h-4 text-gray-500" />;
182+
break;
183+
case 'user':
184+
icon = <User className="w-4 h-4 text-gray-500" />;
185+
break;
186+
default:
187+
icon = <Cloud className="w-4 h-4 text-gray-500" />;
188+
}
189+
190+
return {
191+
name,
192+
value: item.total_cost || 0,
193+
icon
194+
};
195+
});
84196
setBarListData(newData);
85197
}
86-
}, [data]);
198+
}, [data, selectedTab]);
87199

88200
return (
89201
<div className="h-full">
@@ -99,8 +211,8 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
99211
className={({ selected }) =>
100212
`px-4 py-2 text-sm font-medium rounded-lg transition-colors
101213
${selected
102-
? 'bg-tremor-background text-tremor-content-strong shadow'
103-
: 'text-tremor-content hover:bg-tremor-background-subtle'}`
214+
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300'
215+
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}`
104216
}
105217
>
106218
{tab.name}
@@ -113,9 +225,9 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
113225
) : error ? (
114226
<div>Error loading data</div>
115227
) : (
116-
<BarList
228+
<CustomBarList
117229
data={barListData}
118-
valueFormatter={(value: number) => `$${value.toLocaleString()}`} // Add $ sign for cost values
230+
valueFormatter={(value: number) => `$${value.toLocaleString()}`}
119231
onSelectionChange={handleSelectionChange}
120232
/>
121233
)}

tinybird/endpoints/generic_counter.pipe

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ SQL >
3838
AND model in {{Array(model)}}
3939
{% end %}
4040
GROUP BY {{column(dimension, 'organization')}}
41-
ORDER BY count DESC, {{column(dimension, 'organization')}}
41+
ORDER BY total_cost DESC, {{column(dimension, 'organization')}}
4242

4343
TYPE endpoint

0 commit comments

Comments
 (0)