Skip to content

Commit 143ca06

Browse files
authored
Merge pull request #39 from tinybirdco/fixes02
Fixes02
2 parents 4eafa3d + 0d4c26c commit 143ca06

File tree

9 files changed

+208
-127
lines changed

9 files changed

+208
-127
lines changed

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

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
'use client';
22

33
import { Card } from '@tremor/react';
4-
import { useState, useMemo } from 'react';
4+
import { useState, useMemo, useEffect } from 'react';
55
import { RiSearchLine } from '@remixicon/react';
66
import { Dialog, DialogPanel } from '@tremor/react';
7-
import { X } from 'lucide-react';
7+
import { X, Check } from 'lucide-react';
88

99
interface BarListItem {
1010
name: string;
@@ -16,6 +16,7 @@ interface CustomBarListProps {
1616
data: BarListItem[];
1717
valueFormatter?: (value: number) => string;
1818
onSelectionChange?: (selectedItems: string[]) => void;
19+
initialSelectedItems?: string[];
1920
}
2021

2122
const defaultFormatter = (number: number) =>
@@ -24,11 +25,17 @@ const defaultFormatter = (number: number) =>
2425
export default function CustomBarList({
2526
data,
2627
valueFormatter = defaultFormatter,
27-
onSelectionChange
28+
onSelectionChange,
29+
initialSelectedItems = []
2830
}: CustomBarListProps) {
2931
const [isOpen, setIsOpen] = useState(false);
3032
const [searchQuery, setSearchQuery] = useState('');
31-
const [selectedItems, setSelectedItems] = useState<string[]>([]);
33+
const [selectedItems, setSelectedItems] = useState<string[]>(initialSelectedItems);
34+
35+
// Update selected items when initialSelectedItems changes
36+
useEffect(() => {
37+
setSelectedItems(initialSelectedItems);
38+
}, [initialSelectedItems]);
3239

3340
// Memoize filtered items to prevent unnecessary recalculations
3441
const filteredItems = useMemo(() => {
@@ -58,6 +65,11 @@ export default function CustomBarList({
5865
});
5966
};
6067

68+
const handleClearSelection = () => {
69+
setSelectedItems([]);
70+
onSelectionChange?.([]);
71+
};
72+
6173
// Custom bar rendering with icons and improved styling
6274
const renderCustomBarList = (items: BarListItem[]) => (
6375
<div className="mt-4">
@@ -70,11 +82,7 @@ export default function CustomBarList({
7082
return (
7183
<div
7284
key={item.name}
73-
className={`flex flex-col cursor-pointer py-2 transition-all duration-200 ${
74-
isSelected
75-
? 'bg-indigo-50 dark:bg-indigo-900/30 border-l-4 border-indigo-600'
76-
: 'hover:bg-tremor-brand-subtle dark:hover:bg-dark-tremor-brand-subtle border-l-4 border-transparent'
77-
}`}
85+
className={`flex flex-col cursor-pointer py-2 transition-all duration-200 hover:bg-tremor-brand-subtle dark:hover:bg-dark-tremor-brand-subtle`}
7886
onClick={() => handleBarClick(item.name)}
7987
>
8088
<div className="flex items-center w-full py-1">
@@ -87,18 +95,17 @@ export default function CustomBarList({
8795
<p className="truncate small-font" style={{ fontFamily: 'var(--font-family-base)' }}>
8896
{item.name}
8997
</p>
98+
{isSelected && (
99+
<Check className="ml-2 h-4 w-4 text-[var(--accent)]" />
100+
)}
90101
</div>
91-
<p className={`flex-shrink-0 text-right ${
92-
isSelected ? 'text-indigo-600 dark:text-indigo-400' : 'small-font'
93-
}`}>
102+
<p className="flex-shrink-0 text-right small-font">
94103
{valueFormatter(item.value)}
95104
</p>
96105
</div>
97106
<div className="w-full h-1.5 bg-tremor-brand-emphasis dark:bg-dark-tremor-brand-emphasis overflow-hidden">
98107
<div
99-
className={`h-full ${
100-
isSelected ? 'bg-indigo-600' : 'bg-[var(--accent)]'
101-
}`}
108+
className="h-full bg-[var(--accent)]"
102109
style={{ width: `${percentage}%` }}
103110
/>
104111
</div>
@@ -115,7 +122,23 @@ export default function CustomBarList({
115122
style={{ boxShadow: 'none' }}
116123
>
117124
<div className="flex items-center justify-between mb-4">
118-
<h3 className="small-font" style={{ fontFamily: 'var(--font-family-base)' }}>Cost Breakdown</h3>
125+
<div className="flex items-center">
126+
<h3 className="small-font" style={{ fontFamily: 'var(--font-family-base)' }}>Cost Breakdown</h3>
127+
{selectedItems.length > 0 && (
128+
<div className="inline-flex items-center gap-2 px-[10px] py-1.5 ml-2 rounded-full bg-[#393939] font-['Roboto'] text-xs text-[#C6C6C6] hover:text-[var(--accent)] border border-transparent hover:bg-transparent hover:border hover:border-[var(--accent)] transition-colors">
129+
<span>{selectedItems.length} selected</span>
130+
<button
131+
onClick={(e) => {
132+
e.stopPropagation();
133+
handleClearSelection();
134+
}}
135+
aria-label="Clear selection"
136+
>
137+
<X className="h-4 w-4" />
138+
</button>
139+
</div>
140+
)}
141+
</div>
119142
<p className="text-tremor-metric">
120143
{valueFormatter(totalValue)}
121144
</p>
@@ -130,6 +153,20 @@ export default function CustomBarList({
130153
onClick={() => setIsOpen(true)}
131154
>
132155
View All ({data.length})
156+
{selectedItems.length > 0 && (
157+
<div className="inline-flex items-center gap-2 px-[10px] py-1.5 ml-2 rounded-full bg-[#393939] font-['Roboto'] text-xs text-[#C6C6C6] hover:text-[var(--accent)] border border-transparent hover:bg-transparent hover:border hover:border-[var(--accent)] transition-colors">
158+
<span>{selectedItems.length} selected</span>
159+
<button
160+
onClick={(e) => {
161+
e.stopPropagation();
162+
handleClearSelection();
163+
}}
164+
aria-label="Clear selection"
165+
>
166+
<X className="h-4 w-4" />
167+
</button>
168+
</div>
169+
)}
133170
</button>
134171
</div>
135172
)}
@@ -147,7 +184,23 @@ export default function CustomBarList({
147184
<DialogPanel className="!bg-[#262626] flex flex-col relative z-10 rounded-none p-0" style={{ width: '575px', minWidth: '575px' }}>
148185
{/* Header */}
149186
<div className="flex items-center justify-between p-4 pb-0">
150-
<h2 className="title-font">All Items</h2>
187+
<div className="flex items-center">
188+
<h2 className="title-font">All Items</h2>
189+
{selectedItems.length > 0 && (
190+
<div className="inline-flex items-center gap-2 px-[10px] py-1.5 ml-2 rounded-full bg-[#393939] font-['Roboto'] text-xs text-[#C6C6C6] hover:text-[var(--accent)] border border-transparent hover:bg-transparent hover:border hover:border-[var(--accent)] transition-colors">
191+
<span>{selectedItems.length} selected</span>
192+
<button
193+
onClick={(e) => {
194+
e.stopPropagation();
195+
handleClearSelection();
196+
}}
197+
aria-label="Clear selection"
198+
>
199+
<X className="h-4 w-4" />
200+
</button>
201+
</div>
202+
)}
203+
</div>
151204
<button
152205
onClick={() => {
153206
setIsOpen(false);

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

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { useState } from 'react';
3333
value: string;
3434
className?: string;
3535
unit?: string;
36+
isLoading?: boolean;
3637
}
3738

3839
export default function SparkChart({
@@ -42,7 +43,8 @@ import { useState } from 'react';
4243
title,
4344
value,
4445
className,
45-
unit = ''
46+
unit = '',
47+
isLoading = false
4648
}: SparkChartProps) {
4749
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
4850

@@ -70,48 +72,60 @@ import { useState } from 'react';
7072

7173
return (
7274
<Card className={`h-full w-full rounded-none p-4 pb-5 ${className}`} style={{ boxShadow: 'none' }}>
73-
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content" style={{ fontFamily: 'var(--font-family-base)' }}>
74-
{title}
75-
</p>
76-
<p className="text-tremor-metric text-tremor-content-strong dark:text-dark-tremor-content-strong pt-[6px]">
77-
{value}
78-
</p>
79-
<div className="mt-[20px] relative" onMouseMove={handleMouseMove}>
80-
<ChartComponent
81-
data={data}
82-
index="date"
83-
categories={categories}
84-
showGradient={true}
85-
colors={colors}
86-
className="h-28 w-full"
87-
showXAxis={true}
88-
showYAxis={true}
89-
showLegend={false}
90-
showGridLines={true}
91-
showAnimation={false}
92-
curveType="monotone"
93-
stack={isStacked}
94-
customTooltip={(props) => (
95-
<div style={{
96-
position: 'fixed',
97-
left: `${mousePosition.x}px`,
98-
top: `${mousePosition.y}px`,
99-
zIndex: 9999,
100-
pointerEvents: 'none'
101-
}}>
102-
<CustomTooltip
103-
date={props.payload?.[0]?.payload.date}
104-
unit={unit}
105-
entries={props.payload?.map(entry => ({
106-
name: String(entry.name),
107-
value: Array.isArray(entry.value) ? entry.value[0] || 0 : entry.value || 0,
108-
color: entry.color || '#27F795'
109-
})) || []}
110-
/>
111-
</div>
112-
)}
113-
/>
114-
</div>
75+
{isLoading ? (
76+
<div className="h-[148px] w-full flex items-center justify-center bg-[#262626]">
77+
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[var(--accent)]"></div>
78+
</div>
79+
) : !data?.length ? (
80+
<div className="h-[148px] w-full flex items-center justify-center">
81+
<p className="text-[#C6C6C6]">No data available</p>
82+
</div>
83+
) : (
84+
<>
85+
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content" style={{ fontFamily: 'var(--font-family-base)' }}>
86+
{title}
87+
</p>
88+
<p className="text-tremor-metric text-tremor-content-strong dark:text-dark-tremor-content-strong pt-[6px]">
89+
{value}
90+
</p>
91+
<div className="mt-[20px] relative" onMouseMove={handleMouseMove}>
92+
<ChartComponent
93+
data={data}
94+
index="date"
95+
categories={categories}
96+
showGradient={true}
97+
colors={colors}
98+
className="h-28 w-full"
99+
showXAxis={true}
100+
showYAxis={true}
101+
showLegend={false}
102+
showGridLines={true}
103+
showAnimation={false}
104+
curveType="monotone"
105+
stack={isStacked}
106+
customTooltip={(props) => (
107+
<div style={{
108+
position: 'fixed',
109+
left: `${mousePosition.x}px`,
110+
top: `${mousePosition.y}px`,
111+
zIndex: 9999,
112+
pointerEvents: 'none'
113+
}}>
114+
<CustomTooltip
115+
date={props.payload?.[0]?.payload.date}
116+
unit={unit}
117+
entries={props.payload?.map(entry => ({
118+
name: String(entry.name),
119+
value: Array.isArray(entry.value) ? entry.value[0] || 0 : entry.value || 0,
120+
color: entry.color || '#27F795'
121+
})) || []}
122+
/>
123+
</div>
124+
)}
125+
/>
126+
</div>
127+
</>
128+
)}
115129
</Card>
116130
);
117131
}

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,14 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
108108
const initialDimension = searchParams.get('dimension') || filteredTabs[0].key;
109109
const [selectedTab, setSelectedTab] = useState<string>(initialDimension);
110110
const [barListData, setBarListData] = useState<Array<{ name: string; value: number; icon?: React.ReactNode }>>([]);
111-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
112111
const [selectedValues, setSelectedValues] = useState<string[]>([]);
113112

114-
// Pass all filters to the query
115-
const { data, isLoading, error } = useGenericCounter(selectedTab, filters as Record<string, string>);
113+
// Create a copy of filters without the current dimension to avoid filtering by it
114+
const queryFilters = { ...filters };
115+
delete queryFilters[selectedTab];
116+
117+
// Pass all filters to the query, but exclude the current dimension
118+
const { data, isLoading, error } = useGenericCounter(selectedTab, queryFilters as Record<string, string>);
116119

117120
// Add effect to sync with URL params
118121
useEffect(() => {
@@ -144,11 +147,6 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
144147
onFilterUpdate(selectedTab, filteredTabs.find(t => t.key === selectedTab)?.name || selectedTab, newSelection);
145148
};
146149

147-
// const handleRemoveFilter = (dimension: string, value: string) => {
148-
// const newSelection = selectedValues.filter(v => v !== value);
149-
// handleSelectionChange(newSelection);
150-
// };
151-
152150
const handleTabChange = (index: number) => {
153151
const tab = filteredTabs[index];
154152
const dimension = tab.key;
@@ -240,6 +238,7 @@ export default function TabbedPane({ filters, onFilterUpdate }: TabbedPaneProps)
240238
data={barListData}
241239
valueFormatter={(value: number) => `$${value.toLocaleString()}`}
242240
onSelectionChange={handleSelectionChange}
241+
initialSelectedItems={selectedValues}
243242
/>
244243
)}
245244
</div>

dashboard/ai-analytics/src/app/containers/SparkChartContainer.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,34 +34,41 @@ export default function SparkChartContainer({
3434
className,
3535
unit = ''
3636
}: SparkChartContainerProps) {
37-
if (isLoading) return <div>Loading...</div>;
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38+
let transformedData: any;
39+
let categories: string[] = [];
40+
let formattedValue: string = '';
41+
let metricValue: number = 0;
42+
let dates: string[] = [];
3843

39-
// Get unique dates and categories
40-
const dates = [...new Set(data.data.map((d: DataPoint) => d.date))].sort();
41-
const categories = [...new Set(data.data.map((d: DataPoint) => d.category))];
44+
if (!isLoading) {
45+
// Get unique dates and categories
46+
dates = [...new Set(data.data.map((d: DataPoint) => d.date))].sort();
47+
categories = [...new Set(data.data.map((d: DataPoint) => d.category))];
4248

43-
// Transform data for the chart
44-
const transformedData = dates.map(date => {
45-
const dayData = data.data.filter((d: DataPoint) => d.date === date);
46-
return {
47-
date: new Date(date).toLocaleDateString('en-US', {
48-
month: 'short',
49-
day: '2-digit'
50-
}),
51-
...categories.reduce((acc, category) => ({
52-
...acc,
53-
[category]: dayData.find(d => d.category === category)?.[metric] || 0
54-
}), {})
55-
};
56-
});
49+
// Transform data for the chart
50+
transformedData = dates.map(date => {
51+
const dayData = data.data.filter((d: DataPoint) => d.date === date);
52+
return {
53+
date: new Date(date).toLocaleDateString('en-US', {
54+
month: 'short',
55+
day: '2-digit'
56+
}),
57+
...categories.reduce((acc, category) => ({
58+
...acc,
59+
[category]: dayData.find(d => d.category === category)?.[metric] || 0
60+
}), {})
61+
};
62+
});
5763

58-
// Calculate metric average/total
59-
const metricValue = data.data.reduce((sum, curr) => sum + curr[metric], 0);
60-
const formattedValue = metric === 'avg_duration'
61-
? `${(metricValue / data.data.length).toFixed(2)} s`
62-
: metric === 'total_tokens'
63-
? `${metricValue.toLocaleString()} tokens`
64-
: metricValue.toLocaleString();
64+
// Calculate metric average/total
65+
metricValue = data.data.reduce((sum, curr) => sum + curr[metric], 0);
66+
formattedValue = metric === 'avg_duration'
67+
? `${(metricValue / data.data.length).toFixed(2)} s`
68+
: metric === 'total_tokens'
69+
? `${metricValue.toLocaleString()} tokens`
70+
: metricValue.toLocaleString();
71+
}
6572

6673
return (
6774
<SparkChart
@@ -72,6 +79,7 @@ export default function SparkChartContainer({
7279
chartType={chartType}
7380
className={className}
7481
unit={unit}
82+
isLoading={isLoading}
7583
/>
7684
);
7785
}

0 commit comments

Comments
 (0)